diff --git a/charms/aodh-k8s/.gitignore b/.gitignore similarity index 84% rename from charms/aodh-k8s/.gitignore rename to .gitignore index 24ff2e41..32543ded 100644 --- a/charms/aodh-k8s/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ venv/ build/ +.idea/ +.vscode/ *.charm .tox/ .coverage __pycache__/ *.py[cod] -.idea -.vscode/ -*.swp +**.swp .stestr/ diff --git a/charms/heat-k8s/.gitreview b/.gitreview similarity index 63% rename from charms/heat-k8s/.gitreview rename to .gitreview index 5afeb9d1..acf6b77a 100644 --- a/charms/heat-k8s/.gitreview +++ b/.gitreview @@ -1,5 +1,5 @@ [gerrit] host=review.opendev.org port=29418 -project=openstack/charm-heat-k8s.git +project=openstack/sunbeam-charms.git defaultbranch=main diff --git a/charms/keystone-k8s/.jujuignore b/.jujuignore similarity index 100% rename from charms/keystone-k8s/.jujuignore rename to .jujuignore diff --git a/charms/aodh-k8s/.stestr.conf b/.stestr.conf similarity index 100% rename from charms/aodh-k8s/.stestr.conf rename to .stestr.conf diff --git a/charms/aodh-k8s/.gitreview b/charms/aodh-k8s/.gitreview deleted file mode 100644 index c7cbdfc4..00000000 --- a/charms/aodh-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-aodh-k8s.git -defaultbranch=main diff --git a/charms/aodh-k8s/.zuul.yaml b/charms/aodh-k8s/.zuul.yaml deleted file mode 100644 index 87b6ace0..00000000 --- a/charms/aodh-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: aodh-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/aodh-k8s/charmcraft.yaml b/charms/aodh-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/aodh-k8s/charmcraft.yaml +++ b/charms/aodh-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/aodh-k8s/fetch-libs.sh b/charms/aodh-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/aodh-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index 4cf26164..00000000 --- a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# Licensed under the Apache2.0, see LICENCE file in charm source for details. -"""Library for the ingress relation. - -This library contains the Requires and Provides classes for handling -the ingress interface. - -Import `IngressRequires` in your charm, with two required options: -- "self" (the charm itself) -- config_dict - -`config_dict` accepts the following keys: -- additional-hostnames -- backend-protocol -- limit-rps -- limit-whitelist -- max-body-size -- owasp-modsecurity-crs -- owasp-modsecurity-custom-rules -- path-routes -- retry-errors -- rewrite-enabled -- rewrite-target -- service-hostname (required) -- service-name (required) -- service-namespace -- service-port (required) -- session-cookie-max-age -- tls-secret-name - -See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions -of each, along with the required type. - -As an example, add the following to `src/charm.py`: -``` -from charms.nginx_ingress_integrator.v0.ingress import IngressRequires - -# In your charm's `__init__` method. -self.ingress = IngressRequires(self, { - "service-hostname": self.config["external_hostname"], - "service-name": self.app.name, - "service-port": 80, - } -) - -# In your charm's `config-changed` handler. -self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) -``` -And then add the following to `metadata.yaml`: -``` -requires: - ingress: - interface: ingress -``` -You _must_ register the IngressRequires class as part of the `__init__` method -rather than, for instance, a config-changed event handler, for the relation -changed event to be properly handled. -""" - -import copy -import logging -from typing import Dict - -from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -INGRESS_RELATION_NAME = "ingress" -INGRESS_PROXY_RELATION_NAME = "ingress-proxy" - -# The unique Charmhub library identifier, never change it -LIBID = "db0af4367506491c91663468fb5caa4c" - -# 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 = 16 - -LOGGER = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "backend-protocol", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "owasp-modsecurity-custom-rules", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - -RELATION_INTERFACES_MAPPINGS = { - "service-hostname": "host", - "service-name": "name", - "service-namespace": "model", - "service-port": "port", -} -RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values()) - - -class IngressAvailableEvent(EventBase): - """IngressAvailableEvent custom event. - - This event indicates the Ingress provider is available. - """ - - -class IngressProxyAvailableEvent(EventBase): - """IngressProxyAvailableEvent custom event. - - This event indicates the IngressProxy provider is available. - """ - - -class IngressBrokenEvent(RelationBrokenEvent): - """IngressBrokenEvent custom event. - - This event indicates the Ingress provider is broken. - """ - - -class IngressCharmEvents(CharmEvents): - """Custom charm events. - - Attrs: - ingress_available: Event to indicate that Ingress is available. - ingress_proxy_available: Event to indicate that IngressProxy is available. - ingress_broken: Event to indicate that Ingress is broken. - """ - - ingress_available = EventSource(IngressAvailableEvent) - ingress_proxy_available = EventSource(IngressProxyAvailableEvent) - ingress_broken = EventSource(IngressBrokenEvent) - - -class IngressRequires(Object): - """This class defines the functionality for the 'requires' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - - Attrs: - model: Juju model where the charm is deployed. - config_dict: Contains all the configuration options for Ingress. - """ - - def __init__(self, charm: CharmBase, config_dict: Dict) -> None: - """Init function for the IngressRequires class. - - Args: - charm: The charm that requires the ingress relation. - config_dict: Contains all the configuration options for Ingress. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - - # Set default values. - default_relation_fields = { - "service-namespace": self.model.name, - } - config_dict.update( - (key, value) - for key, value in default_relation_fields.items() - if key not in config_dict or not config_dict[key] - ) - - self.config_dict = self._convert_to_relation_interface(config_dict) - - @staticmethod - def _convert_to_relation_interface(config_dict: Dict) -> Dict: - """Create a new relation dict that conforms with charm-relation-interfaces. - - Args: - config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces. - - Returns: - The Ingress configuration conforming with charm-relation-interfaces. - """ - config_dict = copy.copy(config_dict) - config_dict.update( - (key, config_dict[old_key]) - for old_key, key in RELATION_INTERFACES_MAPPINGS.items() - if old_key in config_dict and config_dict[old_key] - ) - return config_dict - - def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool: - """Check our config dict for errors. - - Args: - config_dict: Contains all the configuration options for Ingress. - update_only: If the charm needs to update only existing keys. - - Returns: - If we need to update the config dict or not. - """ - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - config_key - for config_key in config_dict - if config_key - not in REQUIRED_INGRESS_RELATION_FIELDS - | OPTIONAL_INGRESS_RELATION_FIELDS - | RELATION_INTERFACES_MAPPINGS_VALUES - ] - if unknown: - LOGGER.error( - "Ingress relation error, unknown key(s) in config dictionary found: %s", - ", ".join(unknown), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - if not update_only: - missing = tuple( - config_key - for config_key in REQUIRED_INGRESS_RELATION_FIELDS - if config_key not in self.config_dict - ) - if missing: - LOGGER.error( - "Ingress relation error, missing required key(s) in config dictionary: %s", - ", ".join(sorted(missing)), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle the relation-changed event. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(config_dict=self.config_dict): - return - event.relation.data[self.model.app].update( - (key, str(self.config_dict[key])) for key in self.config_dict - ) - - def update_config(self, config_dict: Dict) -> None: - """Allow for updates to relation. - - Args: - config_dict: Contains all the configuration options for Ingress. - - Attrs: - config_dict: Contains all the configuration options for Ingress. - """ - if self.model.unit.is_leader(): - self.config_dict = self._convert_to_relation_interface(config_dict) - if self._config_dict_errors(self.config_dict, update_only=True): - return - relation = self.model.get_relation(INGRESS_RELATION_NAME) - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressBaseProvides(Object): - """Parent class for IngressProvides and IngressProxyProvides. - - Attrs: - model: Juju model where the charm is deployed. - """ - - def __init__(self, charm: CharmBase, relation_name: str) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, relation_name) - self.charm = charm - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle a change to the ingress/ingress-proxy relation. - - Confirm we have the fields we expect to receive. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - relation_name = event.relation.name - - assert event.app is not None # nosec - if not event.relation.data[event.app]: - LOGGER.info( - "%s hasn't finished configuring, waiting until relation is changed again.", - relation_name, - ) - return - - ingress_data = { - field: event.relation.data[event.app].get(field) - for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - } - - missing_fields = sorted( - field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None - ) - - if missing_fields: - LOGGER.warning( - "Missing required data fields for %s relation: %s", - relation_name, - ", ".join(missing_fields), - ) - self.model.unit.status = BlockedStatus( - f"Missing fields for {relation_name}: {', '.join(missing_fields)}" - ) - - if relation_name == INGRESS_RELATION_NAME: - # Conform to charm-relation-interfaces. - if "name" in ingress_data and "port" in ingress_data: - name = ingress_data["name"] - port = ingress_data["port"] - else: - name = ingress_data["service-name"] - port = ingress_data["service-port"] - event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/" - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - elif relation_name == INGRESS_PROXY_RELATION_NAME: - self.charm.on.ingress_proxy_available.emit() - - -class IngressProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress' relation. - - Attrs: - charm: The charm that provides the ingress relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProvides class. - - Args: - charm: The charm that provides the ingress relation. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken - ) - - def _on_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle a relation-broken event in the ingress relation. - - Args: - event: Event triggering the relation-broken hook for the relation. - """ - if not self.model.unit.is_leader(): - return - - # Create an event that our charm can use to remove the ingress resource. - self.charm.on.ingress_broken.emit(event.relation) - - -class IngressProxyProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation. - - Attrs: - charm: The charm that provides the ingress-proxy relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, INGRESS_PROXY_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed - ) diff --git a/charms/aodh-k8s/osci.yaml b/charms/aodh-k8s/osci.yaml deleted file mode 100644 index b59f7058..00000000 --- a/charms/aodh-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: aodh-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/aodh-k8s/rename.sh b/charms/aodh-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/aodh-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/aodh-k8s/requirements.txt b/charms/aodh-k8s/requirements.txt index 5806e426..1dcec118 100644 --- a/charms/aodh-k8s/requirements.txt +++ b/charms/aodh-k8s/requirements.txt @@ -1,9 +1,11 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pydantic<2.0 # Uncomment below if charm relates to ceph # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/aodh-k8s/src/templates/parts/section-service-user b/charms/aodh-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 165fbe71..00000000 --- a/charms/aodh-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,15 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/aodh-k8s/test-requirements.txt b/charms/aodh-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/aodh-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/aodh-k8s/tests/unit/test_charm.py b/charms/aodh-k8s/tests/unit/test_charm.py index a5967b30..0329067b 100644 --- a/charms/aodh-k8s/tests/unit/test_charm.py +++ b/charms/aodh-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Tests for gnocchi charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _AodhOperatorCharm(charm.AodhOperatorCharm): diff --git a/charms/aodh-k8s/tox.ini b/charms/aodh-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/aodh-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/barbican-k8s/.flake8 b/charms/barbican-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/barbican-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/barbican-k8s/.gitignore b/charms/barbican-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/barbican-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/barbican-k8s/.gitreview b/charms/barbican-k8s/.gitreview deleted file mode 100644 index dbfd1ea0..00000000 --- a/charms/barbican-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-barbican-k8s.git -defaultbranch=main diff --git a/charms/barbican-k8s/.jujuignore b/charms/barbican-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/barbican-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/barbican-k8s/.stestr.conf b/charms/barbican-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/barbican-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/barbican-k8s/.zuul.yaml b/charms/barbican-k8s/.zuul.yaml deleted file mode 100644 index c7711b8f..00000000 --- a/charms/barbican-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: barbican-k8s - juju_channel: 3.2/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/barbican-k8s/charmcraft.yaml b/charms/barbican-k8s/charmcraft.yaml index 58193c73..de20145a 100644 --- a/charms/barbican-k8s/charmcraft.yaml +++ b/charms/barbican-k8s/charmcraft.yaml @@ -20,4 +20,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/barbican-k8s/fetch-libs.sh b/charms/barbican-k8s/fetch-libs.sh deleted file mode 100755 index a573dd64..00000000 --- a/charms/barbican-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -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.v0.identity_resource -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 154fab83..00000000 --- a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,392 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": <request id> - "tag": <string to identify request> - "ops": [ - { - "name": <op name>, - "params": { - <param 1>: <value 1>, - <param 2>: <value 2> - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 3 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/barbican-k8s/osci.yaml b/charms/barbican-k8s/osci.yaml deleted file mode 100644 index 7e1239dd..00000000 --- a/charms/barbican-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: barbican-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/barbican-k8s/pyproject.toml b/charms/barbican-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/barbican-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/barbican-k8s/rename.sh b/charms/barbican-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/barbican-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/barbican-k8s/requirements.txt b/charms/barbican-k8s/requirements.txt index 0f23be49..b91c068e 100644 --- a/charms/barbican-k8s/requirements.txt +++ b/charms/barbican-k8s/requirements.txt @@ -13,4 +13,6 @@ lightkube-models ops pwgen pytest-interface-tester -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/barbican-k8s/src/templates/barbican.conf b/charms/barbican-k8s/src/templates/barbican.conf index 0253b41d..bd76ac38 100644 --- a/charms/barbican-k8s/src/templates/barbican.conf +++ b/charms/barbican-k8s/src/templates/barbican.conf @@ -10,6 +10,8 @@ sql_connection = {{ database.connection }} db_auto_create = false {% include "parts/section-identity" %} +# XXX Region should come from the id relation here +region_name = {{ options.region }} {% include "parts/section-service-user" %} diff --git a/charms/barbican-k8s/src/templates/parts/section-identity b/charms/barbican-k8s/src/templates/parts/section-identity deleted file mode 100644 index 92dabb09..00000000 --- a/charms/barbican-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,27 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - -# XXX Region should come from the id relation here -region_name = {{ options.region }} diff --git a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/barbican-k8s/test-requirements.txt b/charms/barbican-k8s/test-requirements.txt deleted file mode 100644 index 0b8ca0cd..00000000 --- a/charms/barbican-k8s/test-requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops diff --git a/charms/barbican-k8s/tests/unit/test_barbican_charm.py b/charms/barbican-k8s/tests/unit/test_barbican_charm.py index eff64acc..6dcaaa76 100644 --- a/charms/barbican-k8s/tests/unit/test_barbican_charm.py +++ b/charms/barbican-k8s/tests/unit/test_barbican_charm.py @@ -16,9 +16,8 @@ """Unit tests for Barbican operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _BarbicanTestOperatorCharm(charm.BarbicanOperatorCharm): diff --git a/charms/barbican-k8s/tox.ini b/charms/barbican-k8s/tox.ini deleted file mode 100644 index fbaa02c5..00000000 --- a/charms/barbican-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ceilometer-k8s/.gitignore b/charms/ceilometer-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/ceilometer-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/ceilometer-k8s/.gitreview b/charms/ceilometer-k8s/.gitreview deleted file mode 100644 index 1b4bcda6..00000000 --- a/charms/ceilometer-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-ceilometer-k8s.git -defaultbranch=main diff --git a/charms/ceilometer-k8s/.stestr.conf b/charms/ceilometer-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ceilometer-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ceilometer-k8s/.zuul.yaml b/charms/ceilometer-k8s/.zuul.yaml deleted file mode 100644 index 18a4323b..00000000 --- a/charms/ceilometer-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ceilometer-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ceilometer-k8s/charmcraft.yaml b/charms/ceilometer-k8s/charmcraft.yaml index ac49568b..98bf024f 100644 --- a/charms/ceilometer-k8s/charmcraft.yaml +++ b/charms/ceilometer-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ceilometer-k8s/fetch-libs.sh b/charms/ceilometer-k8s/fetch-libs.sh deleted file mode 100755 index 16cf2cad..00000000 --- a/charms/ceilometer-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.gnocchi_k8s.v0.gnocchi_service -# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -# charmcraft fetch-lib charms.traefik_k8s.v1.ingress diff --git a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/ceilometer-k8s/osci.yaml b/charms/ceilometer-k8s/osci.yaml deleted file mode 100644 index 05db36a3..00000000 --- a/charms/ceilometer-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ceilometer-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/ceilometer-k8s/pyproject.toml b/charms/ceilometer-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/ceilometer-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ceilometer-k8s/rename.sh b/charms/ceilometer-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ceilometer-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ceilometer-k8s/requirements.txt b/charms/ceilometer-k8s/requirements.txt index 20a477f7..9e7a45a8 100644 --- a/charms/ceilometer-k8s/requirements.txt +++ b/charms/ceilometer-k8s/requirements.txt @@ -1,8 +1,10 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube # Uncomment below if charm relates to ceph # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/ceilometer-k8s/src/templates/ceilometer.conf b/charms/ceilometer-k8s/src/templates/ceilometer.conf index f976d370..1a1b265e 100644 --- a/charms/ceilometer-k8s/src/templates/ceilometer.conf +++ b/charms/ceilometer-k8s/src/templates/ceilometer.conf @@ -20,8 +20,8 @@ archive_policy = low [keystone_authtoken] {% include "parts/identity-data-id-creds" %} -{% include "parts/section-service-user-id-creds" %} +{% include "parts/section-service-user-from-identity-credentials" %} -{% include "parts/section-service-credentials" %} +{% include "parts/section-service-credentials-from-identity-service" %} {% include "parts/section-oslo-messaging-rabbit" %} diff --git a/charms/ceilometer-k8s/src/templates/parts/database-connection b/charms/ceilometer-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data b/charms/ceilometer-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/ceilometer-k8s/src/templates/parts/section-database b/charms/ceilometer-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/ceilometer-k8s/src/templates/parts/section-federation b/charms/ceilometer-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-identity b/charms/ceilometer-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-middleware b/charms/ceilometer-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user b/charms/ceilometer-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 165fbe71..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,15 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-signing b/charms/ceilometer-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/ceilometer-k8s/test-requirements.txt b/charms/ceilometer-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/ceilometer-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ceilometer-k8s/tests/unit/test_charm.py b/charms/ceilometer-k8s/tests/unit/test_charm.py index ff3e7847..4a287b2d 100644 --- a/charms/ceilometer-k8s/tests/unit/test_charm.py +++ b/charms/ceilometer-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Tests for gnocchi charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm): diff --git a/charms/ceilometer-k8s/tox.ini b/charms/ceilometer-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/ceilometer-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/cinder-ceph-k8s/.flake8 b/charms/cinder-ceph-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/cinder-ceph-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/cinder-ceph-k8s/.gitignore b/charms/cinder-ceph-k8s/.gitignore deleted file mode 100644 index ba40a601..00000000 --- a/charms/cinder-ceph-k8s/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -*.charm - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr diff --git a/charms/cinder-ceph-k8s/.gitreview b/charms/cinder-ceph-k8s/.gitreview deleted file mode 100644 index 3b9d90fa..00000000 --- a/charms/cinder-ceph-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-cinder-ceph-k8s.git -defaultbranch=main diff --git a/charms/cinder-ceph-k8s/.jujuignore b/charms/cinder-ceph-k8s/.jujuignore deleted file mode 100644 index 56ce099b..00000000 --- a/charms/cinder-ceph-k8s/.jujuignore +++ /dev/null @@ -1,5 +0,0 @@ -/venv -*.py[cod] -*.charm -.tox -.stestr diff --git a/charms/cinder-ceph-k8s/.stestr.conf b/charms/cinder-ceph-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/cinder-ceph-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/cinder-ceph-k8s/.zuul.yaml b/charms/cinder-ceph-k8s/.zuul.yaml deleted file mode 100644 index 3176ef44..00000000 --- a/charms/cinder-ceph-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: cinder-ceph-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/cinder-ceph-k8s/charmcraft.yaml b/charms/cinder-ceph-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/cinder-ceph-k8s/charmcraft.yaml +++ b/charms/cinder-ceph-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/cinder-ceph-k8s/fetch-libs.sh b/charms/cinder-ceph-k8s/fetch-libs.sh deleted file mode 100755 index defa6987..00000000 --- a/charms/cinder-ceph-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.cinder_k8s.v0.storage_backend -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/cinder-ceph-k8s/osci.yaml b/charms/cinder-ceph-k8s/osci.yaml deleted file mode 100644 index 2d77a6de..00000000 --- a/charms/cinder-ceph-k8s/osci.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - charm-unit-jobs-py38 - - charm-unit-jobs-py310 - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: cinder-ceph-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/cinder-ceph-k8s/pyproject.toml b/charms/cinder-ceph-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/cinder-ceph-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/cinder-ceph-k8s/rename.sh b/charms/cinder-ceph-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/cinder-ceph-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/cinder-ceph-k8s/requirements.txt b/charms/cinder-ceph-k8s/requirements.txt index e3e15ef9..6a1de2d9 100644 --- a/charms/cinder-ceph-k8s/requirements.txt +++ b/charms/cinder-ceph-k8s/requirements.txt @@ -11,7 +11,6 @@ lightkube lightkube-models requests # Drop - not needed in storage backend interface. ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates @@ -23,3 +22,6 @@ git+https://github.com/juju/charm-helpers.git#egg=charmhelpers # TODO requests # Drop - not needed in storage backend interface. netifaces # Drop when charmhelpers dependency is removed. + +# From ops_sunbeam +tenacity diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/cinder-ceph-k8s/test-requirements.txt b/charms/cinder-ceph-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/cinder-ceph-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py index 0fd046cd..7eaad75c 100644 --- a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py +++ b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py @@ -18,16 +18,16 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( + MagicMock, patch, ) from ops.testing import ( Harness, ) -import charm - class _CinderCephOperatorCharm(charm.CinderCephOperatorCharm): """Charm wrapper for test usage.""" @@ -82,6 +82,7 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): def setUp(self): """Setup fixtures ready for testing.""" super().setUp(charm, self.PATCHES) + self.mock_event = MagicMock() self.harness = test_utils.get_harness( _CinderCephOperatorCharm, container_calls=self.container_calls ) @@ -118,7 +119,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): test_utils.add_complete_db_relation(self.harness) add_complete_storage_backend_relation(self.harness) test_utils.set_all_pebbles_ready(self.harness) - self.assertTrue(self.harness.charm.relation_handlers_ready()) + self.assertSetEqual( + self.harness.charm.get_mandatory_relations_not_ready( + self.mock_event + ), + set(), + ) + # self.assertTrue(self.harness.charm.relation_handlers_ready()) def test_ceph_access(self): """Test charm provides secret via ceph-access.""" @@ -132,7 +139,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): ) add_complete_storage_backend_relation(self.harness) test_utils.set_all_pebbles_ready(self.harness) - self.assertTrue(self.harness.charm.relation_handlers_ready()) + # self.assertTrue(self.harness.charm.relation_handlers_ready()) + self.assertSetEqual( + self.harness.charm.get_mandatory_relations_not_ready( + self.mock_event + ), + set(), + ) rel_data = self.harness.get_relation_data( access_rel, self.harness.charm.unit.app.name ) diff --git a/charms/cinder-ceph-k8s/tox.ini b/charms/cinder-ceph-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/cinder-ceph-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/cinder-k8s/.flake8 b/charms/cinder-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/cinder-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/cinder-k8s/.gitignore b/charms/cinder-k8s/.gitignore deleted file mode 100644 index 844a0b93..00000000 --- a/charms/cinder-k8s/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -venv/ -build/ -.stestr/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] diff --git a/charms/cinder-k8s/.gitreview b/charms/cinder-k8s/.gitreview deleted file mode 100644 index e2834fe8..00000000 --- a/charms/cinder-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-cinder-k8s.git -defaultbranch=main diff --git a/charms/cinder-k8s/.jujuignore b/charms/cinder-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/cinder-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/cinder-k8s/.stestr.conf b/charms/cinder-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/cinder-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/cinder-k8s/.zuul.yaml b/charms/cinder-k8s/.zuul.yaml deleted file mode 100644 index 8fdc72b1..00000000 --- a/charms/cinder-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: cinder-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/cinder-k8s/charmcraft.yaml b/charms/cinder-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/cinder-k8s/charmcraft.yaml +++ b/charms/cinder-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/cinder-k8s/fetch-libs.sh b/charms/cinder-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/cinder-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py deleted file mode 100644 index 8b1aa804..00000000 --- a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py +++ /dev/null @@ -1,189 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -# The unique Charmhub library identifier, never change it -LIBID = "68536ea2f06d40078ccbedd7095e141c" - -# 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 - -import json -import logging -import requests - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -# TODO: add your code here! Happy coding! -class StorageBackendConnectedEvent(EventBase): - """StorageBackend connected Event.""" - - pass - - -class StorageBackendReadyEvent(EventBase): - """StorageBackend ready for use Event.""" - - pass - - -class StorageBackendGoneAwayEvent(EventBase): - """StorageBackend relation has gone-away Event""" - - pass - - -class StorageBackendServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(StorageBackendConnectedEvent) - ready = EventSource(StorageBackendReadyEvent) - goneaway = EventSource(StorageBackendGoneAwayEvent) - - -class StorageBackendRequires(Object): - """ - StorageBackendRequires class - """ - - on = StorageBackendServerEvents() - _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_storage_backend_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_storage_backend_relation_broken, - ) - - def _on_storage_backend_relation_joined(self, event): - """StorageBackend relation joined.""" - logging.debug("StorageBackendRequires on_joined") - self.on.connected.emit() - - def _on_storage_backend_relation_changed(self, event): - """StorageBackend relation changed.""" - logging.debug("StorageBackendRequires on_changed") - self.on.ready.emit() - - def _on_storage_backend_relation_broken(self, event): - """StorageBackend relation broken.""" - logging.debug("StorageBackendRequires on_broken") - self.on.goneaway.emit() - - def set_ready(self) -> None: - """Request access to the StorageBackend server.""" - if self.model.unit.is_leader(): - logging.debug( - "Signalling storage backends that core services are ready" - ) - for relation in self.framework.model.relations[self.relation_name]: - relation.data[self.charm.app]["ready"] = 'true' - - -class APIReadyEvent(EventBase): - """StorageBackendClients Ready Event.""" - - pass - - -class StorageBackendClientEvents(ObjectEvents): - """Events class for `on`""" - - api_ready = EventSource(APIReadyEvent) - - -class StorageBackendProvides(Object): - """ - StorageBackendProvides class - """ - - on = StorageBackendClientEvents() - _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_storage_backend_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_storage_backend_relation_broken, - ) - - def _on_storage_backend_relation_joined(self, event): - """Handle StorageBackend joined.""" - logging.debug("StorageBackendProvides on_joined") - - def remote_ready(self): - relation = self.framework.model.get_relation(self.relation_name) - if relation: - ready = relation.data[relation.app].get("ready") - return ready and json.loads(ready) - return False - - def _on_storage_backend_relation_changed(self, event): - """Handle StorageBackend changed.""" - logging.debug("StorageBackendProvides on_changed") - if self.remote_ready(): - self.on.api_ready.emit() - - def _on_storage_backend_relation_broken(self, event): - """Handle StorageBackend broken.""" - logging.debug("RabbitMQStorageBackendProvides on_departed") - # TODO clear data on the relation diff --git a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c8d2e0b1..00000000 --- a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Library for the ingress relation. - -This library contains the Requires and Provides classes for handling -the ingress interface. - -Import `IngressRequires` in your charm, with two required options: - - "self" (the charm itself) - - config_dict - -`config_dict` accepts the following keys: - - service-hostname (required) - - service-name (required) - - service-port (required) - - additional-hostnames - - limit-rps - - limit-whitelist - - max-body-size - - path-routes - - retry-errors - - rewrite-enabled - - rewrite-target - - service-namespace - - session-cookie-max-age - - tls-secret-name - -See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions -of each, along with the required type. - -As an example, add the following to `src/charm.py`: -``` -from charms.nginx_ingress_integrator.v0.ingress import IngressRequires - -# In your charm's `__init__` method. -self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"], - "service-name": self.app.name, - "service-port": 80}) - -# In your charm's `config-changed` handler. -self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) -``` -And then add the following to `metadata.yaml`: -``` -requires: - ingress: - interface: ingress -``` -You _must_ register the IngressRequires class as part of the `__init__` method -rather than, for instance, a config-changed event handler. This is because -doing so won't get the current relation changed event, because it wasn't -registered to handle the event (because it wasn't created in `__init__` when -the event was fired). -""" - -import logging - -from ops.charm import CharmEvents -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -# The unique Charmhub library identifier, never change it -LIBID = "db0af4367506491c91663468fb5caa4c" - -# 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 = 9 - -logger = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = { - "service-hostname", - "service-name", - "service-port", -} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "limit-rps", - "limit-whitelist", - "max-body-size", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", - "path-routes", -} - - -class IngressAvailableEvent(EventBase): - pass - - -class IngressCharmEvents(CharmEvents): - """Custom charm events.""" - - ingress_available = EventSource(IngressAvailableEvent) - - -class IngressRequires(Object): - """This class defines the functionality for the 'requires' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm, config_dict): - super().__init__(charm, "ingress") - - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - - self.config_dict = config_dict - - def _config_dict_errors(self, update_only=False): - """Check our config dict for errors.""" - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - x - for x in self.config_dict - if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - ] - if unknown: - logger.error( - "Ingress relation error, unknown key(s) in config dictionary found: %s", - ", ".join(unknown), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - if not update_only: - missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict] - if missing: - logger.error( - "Ingress relation error, missing required key(s) in config dictionary: %s", - ", ".join(missing), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - return False - - def _on_relation_changed(self, event): - """Handle the relation-changed event.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(): - return - for key in self.config_dict: - event.relation.data[self.model.app][key] = str(self.config_dict[key]) - - def update_config(self, config_dict): - """Allow for updates to relation.""" - if self.model.unit.is_leader(): - self.config_dict = config_dict - if self._config_dict_errors(update_only=True): - return - relation = self.model.get_relation("ingress") - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressProvides(Object): - """This class defines the functionality for the 'provides' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm): - super().__init__(charm, "ingress") - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - self.charm = charm - - def _on_relation_changed(self, event): - """Handle a change to the ingress relation. - - Confirm we have the fields we expect to receive.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - ingress_data = { - field: event.relation.data[event.app].get(field) - for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - } - - missing_fields = sorted( - [ - field - for field in REQUIRED_INGRESS_RELATION_FIELDS - if ingress_data.get(field) is None - ] - ) - - if missing_fields: - logger.error( - "Missing required data fields for ingress relation: {}".format( - ", ".join(missing_fields) - ) - ) - self.model.unit.status = BlockedStatus( - "Missing fields for ingress: {}".format(", ".join(missing_fields)) - ) - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() diff --git a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/cinder-k8s/osci.yaml b/charms/cinder-k8s/osci.yaml deleted file mode 100644 index 13dbaa2d..00000000 --- a/charms/cinder-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: cinder-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/cinder-k8s/pyproject.toml b/charms/cinder-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/cinder-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/cinder-k8s/rename.sh b/charms/cinder-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/cinder-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/cinder-k8s/requirements.txt b/charms/cinder-k8s/requirements.txt index c92b43df..7743a6fe 100644 --- a/charms/cinder-k8s/requirements.txt +++ b/charms/cinder-k8s/requirements.txt @@ -11,9 +11,11 @@ pydantic<2.0 lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates # TODO requests # Drop - not needed in storage backend interface. + +# From ops_sunbeam +tenacity diff --git a/charms/cinder-k8s/src/templates/parts/section-database b/charms/cinder-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/cinder-k8s/src/templates/parts/section-federation b/charms/cinder-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/cinder-k8s/src/templates/parts/section-identity b/charms/cinder-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/cinder-k8s/src/templates/parts/section-middleware b/charms/cinder-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/cinder-k8s/src/templates/parts/section-service-user b/charms/cinder-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/cinder-k8s/src/templates/parts/section-signing b/charms/cinder-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/cinder-k8s/test-requirements.txt b/charms/cinder-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/cinder-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/cinder-k8s/tests/unit/test_cinder_charm.py b/charms/cinder-k8s/tests/unit/test_cinder_charm.py index 712e21d9..4ff7d581 100644 --- a/charms/cinder-k8s/tests/unit/test_cinder_charm.py +++ b/charms/cinder-k8s/tests/unit/test_cinder_charm.py @@ -16,9 +16,8 @@ """Unit tests for core Cinder charm class.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _CinderOperatorCharm(charm.CinderOperatorCharm): diff --git a/charms/cinder-k8s/tox.ini b/charms/cinder-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/cinder-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/designate-bind-k8s/.gitignore b/charms/designate-bind-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/designate-bind-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/designate-bind-k8s/.gitreview b/charms/designate-bind-k8s/.gitreview deleted file mode 100644 index 15594b28..00000000 --- a/charms/designate-bind-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-designate-bind-k8s.git -defaultbranch=main diff --git a/charms/designate-bind-k8s/.stestr.conf b/charms/designate-bind-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/designate-bind-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/designate-bind-k8s/.zuul.yaml b/charms/designate-bind-k8s/.zuul.yaml deleted file mode 100644 index ef9a2ff4..00000000 --- a/charms/designate-bind-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: designate-bind-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/designate-bind-k8s/charmcraft.yaml b/charms/designate-bind-k8s/charmcraft.yaml index 9c5a769f..e24ab1f5 100644 --- a/charms/designate-bind-k8s/charmcraft.yaml +++ b/charms/designate-bind-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/designate-bind-k8s/fetch-libs.sh b/charms/designate-bind-k8s/fetch-libs.sh deleted file mode 100755 index d13dfc5c..00000000 --- a/charms/designate-bind-k8s/fetch-libs.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.observability-libs.v1.kubernetes_service_patch diff --git a/charms/designate-bind-k8s/osci.yaml b/charms/designate-bind-k8s/osci.yaml deleted file mode 100644 index 4cc088c3..00000000 --- a/charms/designate-bind-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: designate-bind-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 9/edge diff --git a/charms/designate-bind-k8s/pyproject.toml b/charms/designate-bind-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/designate-bind-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/designate-bind-k8s/rename.sh b/charms/designate-bind-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/designate-bind-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/designate-bind-k8s/requirements.txt b/charms/designate-bind-k8s/requirements.txt index cb87fb03..ccdfbaf8 100644 --- a/charms/designate-bind-k8s/requirements.txt +++ b/charms/designate-bind-k8s/requirements.txt @@ -1,5 +1,7 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube lightkube-models + +# From ops_sunbeam +tenacity diff --git a/charms/designate-bind-k8s/test-requirements.txt b/charms/designate-bind-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/designate-bind-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py index 90562b60..5bfb0074 100644 --- a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py +++ b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py @@ -14,9 +14,8 @@ """Unit tests.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _BindTestOperatorCharm(charm.BindOperatorCharm): diff --git a/charms/designate-bind-k8s/tox.ini b/charms/designate-bind-k8s/tox.ini deleted file mode 100644 index b14f9098..00000000 --- a/charms/designate-bind-k8s/tox.ini +++ /dev/null @@ -1,166 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -project_lib_path = {toxinidir}/lib/charms/designate_bind_k8s -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} {[vars]project_lib_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/designate-k8s/.gitignore b/charms/designate-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/designate-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/designate-k8s/.gitreview b/charms/designate-k8s/.gitreview deleted file mode 100644 index 724badba..00000000 --- a/charms/designate-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-designate-k8s.git -defaultbranch=main diff --git a/charms/designate-k8s/.stestr.conf b/charms/designate-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/designate-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/designate-k8s/.zuul.yaml b/charms/designate-k8s/.zuul.yaml deleted file mode 100644 index 210ad0b5..00000000 --- a/charms/designate-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: designate-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/designate-k8s/charmcraft.yaml b/charms/designate-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/designate-k8s/charmcraft.yaml +++ b/charms/designate-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/designate-k8s/fetch-libs.sh b/charms/designate-k8s/fetch-libs.sh deleted file mode 100755 index 5cdf501d..00000000 --- a/charms/designate-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.designate_bind_k8s.v0.bind_rndc - diff --git a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py deleted file mode 100644 index 2bb2f93e..00000000 --- a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py +++ /dev/null @@ -1,364 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -"""BindRndc Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the bind_rndc interface. -Import `BindRndcRequires` in your charm, with the charm object and the -relation name: - - self - - "dns-backend" -Two events are also available to respond to: - - bind_rndc_ready - - goneaway -A basic example showing the usage of this relation follows: -``` -from charms.designate_bind_k8s.v0.bind_rndc import ( - BindRndcRequires -) -class BindRndcClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # BindRndc Requires - self.bind_rndc = BindRndcRequires( - self, "dns-backend" - ) - self.framework.observe( - self.bind_rndc.on.bind_rndc_ready, - self._on_bind_rndc_ready - ) - self.framework.observe( - self.bind_rndc.on.goneaway, - self._on_bind_rndc_goneaway - ) - def _on_bind_rndc_connected(self, event): - '''React to the Bind Rndc Connected event. - This event happens when BindRndc relation is added to the - model. - ''' - # Request the rndc key from the Bind Rndc relation. - self.bind_rndc.request_rndc_key("generated nonce") - def _on_bind_rndc_ready(self, event): - '''React to the Bind Rndc Ready event. - This event happens when BindRndc relation is added to the - model, relation is ready and/or relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - def _on_bind_rndc_goneaway(self, event): - '''React to the BindRndc goneaway event. - This event happens when BindRndc relation is removed. - ''' - # BindRndc Relation has goneaway. - pass -``` -""" - -import json -import logging -from typing import ( - Any, - Dict, - List, - Optional, - Union, -) - -import ops - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "1cb766c981874e7383d17cf54148b3d4" - -# 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 - - -class BindRndcConnectedEvent(ops.EventBase): - """Bind rndc connected event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcReadyEvent(ops.EventBase): - """Bind rndc ready event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcGoneAwayEvent(ops.EventBase): - """Bind rndc gone away event.""" - - pass - - -class BindRndcRequirerEvents(ops.ObjectEvents): - """List of events that the BindRndc requires charm can leverage.""" - - connected = ops.EventSource(BindRndcConnectedEvent) - ready = ops.EventSource(BindRndcReadyEvent) - goneaway = ops.EventSource(BindRndcGoneAwayEvent) - - -class BindRndcRequires(ops.Object): - """Class to be instantiated by the requiring side of the relation.""" - - on = BindRndcRequirerEvents() - - def __init__(self, charm: ops.CharmBase, 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_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_relation_broken, - ) - - def _on_relation_joined(self, event: ops.RelationJoinedEvent): - """Handle relation joined event.""" - self.on.connected.emit( - event.relation.id, - event.relation.name, - ) - - def _on_relation_changed(self, event: ops.RelationJoinedEvent): - """Handle relation changed event.""" - host = self.host(event.relation) - rndc_key = self.get_rndc_key(event.relation) - - if all((host, rndc_key)): - self.on.ready.emit( - event.relation.id, - event.relation.name, - ) - - def _on_relation_broken(self, event: ops.RelationBrokenEvent): - """Handle relation broken event.""" - self.on.goneaway.emit() - - def host(self, relation: ops.Relation) -> Optional[str]: - """Return host from relation.""" - if relation.app is None: - return None - return relation.data[relation.app].get("host") - - def nonce(self, relation: ops.Relation) -> Optional[str]: - """Return nonce from relation.""" - return relation.data[self.charm.unit].get("nonce") - - def get_rndc_key(self, relation: ops.Relation) -> Optional[dict]: - """Get rndc keys.""" - if relation.app is None: - return None - if self.nonce(relation) is None: - logger.debug("No nonce set for unit yet") - return None - - return json.loads( - relation.data[relation.app].get("rndc_keys", "{}") - ).get(self.nonce(relation)) - - def request_rndc_key(self, relation: ops.Relation, nonce: str): - """Request rndc key over the relation.""" - relation.data[self.charm.unit]["nonce"] = nonce - - -class NewBindClientAttachedEvent(ops.EventBase): - """New bind client attached event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindClientUpdatedEvent(ops.EventBase): - """Bind client updated event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcProviderEvents(ops.ObjectEvents): - """List of events that the BindRndc provider charm can leverage.""" - - new_bind_client_attached = ops.EventSource(NewBindClientAttachedEvent) - bind_client_updated = ops.EventSource(BindClientUpdatedEvent) - - -class BindRndcProvides(ops.Object): - """Class to be instantiated by the providing side of the relation.""" - - on = BindRndcProviderEvents() - - def __init__(self, charm: ops.CharmBase, 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_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - - def _on_relation_joined(self, event: ops.RelationJoinedEvent): - self.on.new_bind_client_attached.emit( - event.relation.id, event.relation.name - ) - - def _on_relation_changed(self, event: ops.RelationChangedEvent): - self.on.bind_client_updated.emit( - event.relation.id, event.relation.name - ) - - def set_host(self, relation: ops.Relation, host: str): - """Set host on the relation.""" - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping set_host") - return - relation.data[self.charm.app]["host"] = host - - def get_rndc_keys(self, relation: ops.Relation) -> dict: - """Get rndc keys.""" - return json.loads(relation.data[self.charm.app].get("rndc_keys", "{}")) - - def set_rndc_client_key( - self, - relation: ops.Relation, - client: str, - algorithm: str, - secret: ops.Secret, - ): - """Add rndc key to the relation. - - `rndc_keys` is a dict of dicts, keyed by client name. Each client - has an algorithm and secret property. The secret is a Juju secret id, - containing the actual secret needed to communicate over rndc. - """ - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping set_rndc_client_key") - return - - keys = self.get_rndc_keys(relation) - keys[client] = { - "algorithm": algorithm, - "secret": secret.id, - } - - relation.data[self.charm.app]["rndc_keys"] = json.dumps( - keys, sort_keys=True - ) - - def remove_rndc_client_key( - self, - relation: ops.Relation, - client: Union[str, List[str]], - ): - """Remove rndc key from the relation.""" - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping remove_rndc_client_key") - return - if isinstance(client, str): - client = [client] - keys = self.get_rndc_keys(relation) - for c in client: - keys.pop(c) - relation.data[self.charm.app]["rndc_keys"] = json.dumps( - keys, sort_keys=True - ) diff --git a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/designate-k8s/osci.yaml b/charms/designate-k8s/osci.yaml deleted file mode 100644 index 9d3005c7..00000000 --- a/charms/designate-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: designate-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/designate-k8s/pyproject.toml b/charms/designate-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/designate-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/designate-k8s/rename.sh b/charms/designate-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/designate-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/designate-k8s/requirements.txt b/charms/designate-k8s/requirements.txt index db1e74a8..e0ef265f 100644 --- a/charms/designate-k8s/requirements.txt +++ b/charms/designate-k8s/requirements.txt @@ -2,5 +2,7 @@ ops jsonschema pydantic<2.0 jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube + +# From ops_sunbeam +tenacity diff --git a/charms/designate-k8s/src/templates/parts/database-connection b/charms/designate-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/designate-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/designate-k8s/src/templates/parts/identity-data b/charms/designate-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/designate-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/designate-k8s/src/templates/parts/section-database b/charms/designate-k8s/src/templates/parts/section-database deleted file mode 100644 index b060b139..00000000 --- a/charms/designate-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,4 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 -db_auto_create = false diff --git a/charms/designate-k8s/src/templates/parts/section-federation b/charms/designate-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/designate-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/designate-k8s/src/templates/parts/section-identity b/charms/designate-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/designate-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/designate-k8s/src/templates/parts/section-middleware b/charms/designate-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/designate-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/designate-k8s/src/templates/parts/section-service-user b/charms/designate-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/designate-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/designate-k8s/src/templates/parts/section-signing b/charms/designate-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/designate-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/designate-k8s/test-requirements.txt b/charms/designate-k8s/test-requirements.txt deleted file mode 100644 index f33eee5a..00000000 --- a/charms/designate-k8s/test-requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -pytest-mock diff --git a/charms/designate-k8s/tests/unit/test_designate_charm.py b/charms/designate-k8s/tests/unit/test_designate_charm.py index 85a55ac3..05c2367d 100644 --- a/charms/designate-k8s/tests/unit/test_designate_charm.py +++ b/charms/designate-k8s/tests/unit/test_designate_charm.py @@ -18,13 +18,12 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from ops.testing import ( Harness, ) -import charm - class _DesignateTestOperatorCharm(charm.DesignateOperatorCharm): """Test Operator Charm for Designate Operator.""" diff --git a/charms/designate-k8s/tox.ini b/charms/designate-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/designate-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/glance-k8s/.flake8 b/charms/glance-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/glance-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/glance-k8s/.gitignore b/charms/glance-k8s/.gitignore deleted file mode 100644 index 2d3a0ccd..00000000 --- a/charms/glance-k8s/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -.stestr/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] -tempest.log diff --git a/charms/glance-k8s/.gitreview b/charms/glance-k8s/.gitreview deleted file mode 100644 index 48ce75b3..00000000 --- a/charms/glance-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-glance-k8s.git -defaultbranch=main diff --git a/charms/glance-k8s/.jujuignore b/charms/glance-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/glance-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/glance-k8s/.stestr.conf b/charms/glance-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/glance-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/glance-k8s/.zuul.yaml b/charms/glance-k8s/.zuul.yaml deleted file mode 100644 index 38b2ac8d..00000000 --- a/charms/glance-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: glance-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/glance-k8s/charmcraft.yaml b/charms/glance-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/glance-k8s/charmcraft.yaml +++ b/charms/glance-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/glance-k8s/fetch-libs.sh b/charms/glance-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/glance-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/glance-k8s/osci.yaml b/charms/glance-k8s/osci.yaml deleted file mode 100644 index 538d747e..00000000 --- a/charms/glance-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: glance-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/glance-k8s/pyproject.toml b/charms/glance-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/glance-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/glance-k8s/rename.sh b/charms/glance-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/glance-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/glance-k8s/requirements.txt b/charms/glance-k8s/requirements.txt index c9d578be..451445a1 100644 --- a/charms/glance-k8s/requirements.txt +++ b/charms/glance-k8s/requirements.txt @@ -13,11 +13,12 @@ lightkube-models ops netifaces -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates # Note: Required for cinder-ceph-k8s, glance-k8s git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # Charmhelpers is only present as interface_ceph_client uses it. git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py index 62c6eb03..106207c0 100755 --- a/charms/glance-k8s/src/charm.py +++ b/charms/glance-k8s/src/charm.py @@ -338,7 +338,8 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): def configure_charm(self, event) -> None: """Catchall handler to configure charm services.""" - if not self.relation_handlers_ready(): + not_ready_relations = self.get_mandatory_relations_not_ready(event) + if not_ready_relations: logger.debug("Deferring configuration, charm relations not ready") return diff --git a/charms/glance-k8s/src/templates/parts/section-database b/charms/glance-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/glance-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/glance-k8s/src/templates/parts/section-federation b/charms/glance-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/glance-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/glance-k8s/src/templates/parts/section-identity b/charms/glance-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/glance-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/glance-k8s/src/templates/parts/section-middleware b/charms/glance-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/glance-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-notifications b/charms/glance-k8s/src/templates/parts/section-oslo-notifications deleted file mode 100644 index ce559feb..00000000 --- a/charms/glance-k8s/src/templates/parts/section-oslo-notifications +++ /dev/null @@ -1,4 +0,0 @@ -{% if options.enable_telemetry_notifications -%} -[oslo_messaging_notifications] -driver = messagingv2 -{%- endif %} diff --git a/charms/glance-k8s/src/templates/parts/section-service-user b/charms/glance-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/glance-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/glance-k8s/src/templates/parts/section-signing b/charms/glance-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/glance-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/glance-k8s/test-requirements.txt b/charms/glance-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/glance-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/glance-k8s/tests/unit/test_glance_charm.py b/charms/glance-k8s/tests/unit/test_glance_charm.py index 3cd6887c..8d5c3914 100644 --- a/charms/glance-k8s/tests/unit/test_glance_charm.py +++ b/charms/glance-k8s/tests/unit/test_glance_charm.py @@ -16,13 +16,12 @@ """Tests for glance charm.""" +import charm import ops_sunbeam.test_utils as test_utils from mock import ( patch, ) -import charm - class _GlanceOperatorCharm(charm.GlanceOperatorCharm): def __init__(self, framework): diff --git a/charms/glance-k8s/tox.ini b/charms/glance-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/glance-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/gnocchi-k8s/.gitignore b/charms/gnocchi-k8s/.gitignore deleted file mode 100644 index 7d5f287a..00000000 --- a/charms/gnocchi-k8s/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ - diff --git a/charms/gnocchi-k8s/.gitreview b/charms/gnocchi-k8s/.gitreview deleted file mode 100644 index 9b24204b..00000000 --- a/charms/gnocchi-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-gnocchi-k8s.git -defaultbranch=main diff --git a/charms/gnocchi-k8s/.stestr.conf b/charms/gnocchi-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/gnocchi-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/gnocchi-k8s/.zuul.yaml b/charms/gnocchi-k8s/.zuul.yaml deleted file mode 100644 index 4daf525d..00000000 --- a/charms/gnocchi-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: gnocchi-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.26-strict/stable - microk8s_classic_mode: false diff --git a/charms/gnocchi-k8s/charmcraft.yaml b/charms/gnocchi-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/gnocchi-k8s/charmcraft.yaml +++ b/charms/gnocchi-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/gnocchi-k8s/fetch-libs.sh b/charms/gnocchi-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/gnocchi-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py deleted file mode 100644 index fbc679d5..00000000 --- a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py +++ /dev/null @@ -1,205 +0,0 @@ -"""GnocchiService Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the Gnocchi service interface. - -Import `GnocchiServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "gnocchi-db" - -Two events are also available to respond to: - - readiness_changed - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.gnocchi_k8s.v0.gnocchi_service import ( - GnocchiServiceRequires -) - -class GnocchiServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # GnocchiService Requires - self.gnocchi_svc = GnocchiServiceRequires( - self, "gnocchi-db", - ) - self.framework.observe( - self.gnocchi_svc.on.readiness_changed, - self._on_gnocchi_service_readiness_changed - ) - self.framework.observe( - self.gnocchi_svc.on.goneaway, - self._on_gnocchi_service_goneaway - ) - - def _on_gnocchi_service_readiness_changed(self, event): - '''React to the Gnocchi service readiness changed event. - - This event happens when Gnocchi service relation is added to the - model and relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - - def _on_gnocchi_service_goneaway(self, event): - '''React to the Gnocchi Service goneaway event. - - This event happens when Gnocchi service relation is removed. - ''' - # HeatSharedConfig Relation has goneaway. - pass -``` -""" - - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "97b7682b415040f3b32d77fff8d93e7e" - -# 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 = 2 - - -class GnocchiServiceReadinessRequestEvent(RelationEvent): - """GnocchiServiceReadinessRequest Event.""" - - pass - - -class GnocchiServiceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - service_readiness = EventSource(GnocchiServiceReadinessRequestEvent) - - -class GnocchiServiceProvides(Object): - """GnocchiServiceProvides class.""" - - on = GnocchiServiceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_relation_changed, - ) - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle Gnocchi service relation changed.""" - logging.debug("Gnocchi Service relation changed") - self.on.service_readiness.emit(event.relation) - - def set_service_status(self, relation: Relation, is_ready: bool) -> None: - """Set gnocchi service readiness status on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping setting ready status") - return - - logging.debug( - f"Setting ready status on relation {relation.app.name} " - f"{relation.name}/{relation.id}" - ) - relation.data[self.charm.app]["ready"] = json.dumps(is_ready) - - -class GnocchiServiceReadinessChangedEvent(RelationEvent): - """GnocchiServiceReadinessChanged Event.""" - - pass - - -class GnocchiServiceGoneAwayEvent(RelationEvent): - """GnocchiServiceGoneAway Event.""" - - pass - - -class GnocchiServiceRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - readiness_changed = EventSource(GnocchiServiceReadinessChangedEvent) - goneaway = EventSource(GnocchiServiceGoneAwayEvent) - - -class GnocchiServiceRequires(Object): - """GnocchiServiceRequires class.""" - - on = GnocchiServiceRequirerEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_relation_broken, - ) - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle Gnocchi Service relation changed.""" - logging.debug("Gnocchi service readiness data changed") - self.on.readiness_changed.emit(event.relation) - - def _on_relation_broken(self, event: RelationBrokenEvent): - """Handle Gnocchi Service relation broken.""" - logging.debug("Gnocchi service on_broken") - self.on.goneaway.emit(event.relation) - - @property - def _gnocchi_service_rel(self) -> Optional[Relation]: - """The gnocchi service relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._gnocchi_service_rel: - data = self._gnocchi_service_rel.data[ - self._gnocchi_service_rel.app - ] - return data.get(key) - - return None - - @property - def service_ready(self) -> bool: - """Return if gnocchi service is ready or not.""" - is_ready = self.get_remote_app_data("ready") - if is_ready: - return json.loads(is_ready) - - return False diff --git a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/gnocchi-k8s/osci.yaml b/charms/gnocchi-k8s/osci.yaml deleted file mode 100644 index 05207373..00000000 --- a/charms/gnocchi-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: gnocchi-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/gnocchi-k8s/pyproject.toml b/charms/gnocchi-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/gnocchi-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/gnocchi-k8s/rename.sh b/charms/gnocchi-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/gnocchi-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/gnocchi-k8s/requirements.txt b/charms/gnocchi-k8s/requirements.txt index b932d999..6a4bf90a 100644 --- a/charms/gnocchi-k8s/requirements.txt +++ b/charms/gnocchi-k8s/requirements.txt @@ -1,6 +1,5 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube netifaces jsonschema @@ -8,3 +7,6 @@ pydantic<2.0 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/gnocchi-k8s/src/templates/parts/database-connection b/charms/gnocchi-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/gnocchi-k8s/src/templates/parts/identity-data b/charms/gnocchi-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/gnocchi-k8s/src/templates/parts/section-database b/charms/gnocchi-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/gnocchi-k8s/src/templates/parts/section-federation b/charms/gnocchi-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-identity b/charms/gnocchi-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-middleware b/charms/gnocchi-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-signing b/charms/gnocchi-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/gnocchi-k8s/test-requirements.txt b/charms/gnocchi-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/gnocchi-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/gnocchi-k8s/tests/unit/test_charm.py b/charms/gnocchi-k8s/tests/unit/test_charm.py index 2d5da5cc..f0ac97d7 100644 --- a/charms/gnocchi-k8s/tests/unit/test_charm.py +++ b/charms/gnocchi-k8s/tests/unit/test_charm.py @@ -16,13 +16,12 @@ """Tests for gnocchi charm.""" +import charm import ops_sunbeam.test_utils as test_utils from mock import ( patch, ) -import charm - class _GnocchiCephOperatorCharm(charm.GnocchiCephOperatorCharm): def __init__(self, framework): diff --git a/charms/gnocchi-k8s/tox.ini b/charms/gnocchi-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/gnocchi-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/heat-k8s/.gitignore b/charms/heat-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/heat-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/heat-k8s/.stestr.conf b/charms/heat-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/heat-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/heat-k8s/.zuul.yaml b/charms/heat-k8s/.zuul.yaml deleted file mode 100644 index cfece9f8..00000000 --- a/charms/heat-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: heat-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/heat-k8s/charmcraft.yaml b/charms/heat-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/heat-k8s/charmcraft.yaml +++ b/charms/heat-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/heat-k8s/fetch-libs.sh b/charms/heat-k8s/fetch-libs.sh deleted file mode 100755 index 600c0b69..00000000 --- a/charms/heat-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route diff --git a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index 4cf26164..00000000 --- a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# Licensed under the Apache2.0, see LICENCE file in charm source for details. -"""Library for the ingress relation. - -This library contains the Requires and Provides classes for handling -the ingress interface. - -Import `IngressRequires` in your charm, with two required options: -- "self" (the charm itself) -- config_dict - -`config_dict` accepts the following keys: -- additional-hostnames -- backend-protocol -- limit-rps -- limit-whitelist -- max-body-size -- owasp-modsecurity-crs -- owasp-modsecurity-custom-rules -- path-routes -- retry-errors -- rewrite-enabled -- rewrite-target -- service-hostname (required) -- service-name (required) -- service-namespace -- service-port (required) -- session-cookie-max-age -- tls-secret-name - -See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions -of each, along with the required type. - -As an example, add the following to `src/charm.py`: -``` -from charms.nginx_ingress_integrator.v0.ingress import IngressRequires - -# In your charm's `__init__` method. -self.ingress = IngressRequires(self, { - "service-hostname": self.config["external_hostname"], - "service-name": self.app.name, - "service-port": 80, - } -) - -# In your charm's `config-changed` handler. -self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) -``` -And then add the following to `metadata.yaml`: -``` -requires: - ingress: - interface: ingress -``` -You _must_ register the IngressRequires class as part of the `__init__` method -rather than, for instance, a config-changed event handler, for the relation -changed event to be properly handled. -""" - -import copy -import logging -from typing import Dict - -from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -INGRESS_RELATION_NAME = "ingress" -INGRESS_PROXY_RELATION_NAME = "ingress-proxy" - -# The unique Charmhub library identifier, never change it -LIBID = "db0af4367506491c91663468fb5caa4c" - -# 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 = 16 - -LOGGER = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "backend-protocol", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "owasp-modsecurity-custom-rules", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - -RELATION_INTERFACES_MAPPINGS = { - "service-hostname": "host", - "service-name": "name", - "service-namespace": "model", - "service-port": "port", -} -RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values()) - - -class IngressAvailableEvent(EventBase): - """IngressAvailableEvent custom event. - - This event indicates the Ingress provider is available. - """ - - -class IngressProxyAvailableEvent(EventBase): - """IngressProxyAvailableEvent custom event. - - This event indicates the IngressProxy provider is available. - """ - - -class IngressBrokenEvent(RelationBrokenEvent): - """IngressBrokenEvent custom event. - - This event indicates the Ingress provider is broken. - """ - - -class IngressCharmEvents(CharmEvents): - """Custom charm events. - - Attrs: - ingress_available: Event to indicate that Ingress is available. - ingress_proxy_available: Event to indicate that IngressProxy is available. - ingress_broken: Event to indicate that Ingress is broken. - """ - - ingress_available = EventSource(IngressAvailableEvent) - ingress_proxy_available = EventSource(IngressProxyAvailableEvent) - ingress_broken = EventSource(IngressBrokenEvent) - - -class IngressRequires(Object): - """This class defines the functionality for the 'requires' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - - Attrs: - model: Juju model where the charm is deployed. - config_dict: Contains all the configuration options for Ingress. - """ - - def __init__(self, charm: CharmBase, config_dict: Dict) -> None: - """Init function for the IngressRequires class. - - Args: - charm: The charm that requires the ingress relation. - config_dict: Contains all the configuration options for Ingress. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - - # Set default values. - default_relation_fields = { - "service-namespace": self.model.name, - } - config_dict.update( - (key, value) - for key, value in default_relation_fields.items() - if key not in config_dict or not config_dict[key] - ) - - self.config_dict = self._convert_to_relation_interface(config_dict) - - @staticmethod - def _convert_to_relation_interface(config_dict: Dict) -> Dict: - """Create a new relation dict that conforms with charm-relation-interfaces. - - Args: - config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces. - - Returns: - The Ingress configuration conforming with charm-relation-interfaces. - """ - config_dict = copy.copy(config_dict) - config_dict.update( - (key, config_dict[old_key]) - for old_key, key in RELATION_INTERFACES_MAPPINGS.items() - if old_key in config_dict and config_dict[old_key] - ) - return config_dict - - def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool: - """Check our config dict for errors. - - Args: - config_dict: Contains all the configuration options for Ingress. - update_only: If the charm needs to update only existing keys. - - Returns: - If we need to update the config dict or not. - """ - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - config_key - for config_key in config_dict - if config_key - not in REQUIRED_INGRESS_RELATION_FIELDS - | OPTIONAL_INGRESS_RELATION_FIELDS - | RELATION_INTERFACES_MAPPINGS_VALUES - ] - if unknown: - LOGGER.error( - "Ingress relation error, unknown key(s) in config dictionary found: %s", - ", ".join(unknown), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - if not update_only: - missing = tuple( - config_key - for config_key in REQUIRED_INGRESS_RELATION_FIELDS - if config_key not in self.config_dict - ) - if missing: - LOGGER.error( - "Ingress relation error, missing required key(s) in config dictionary: %s", - ", ".join(sorted(missing)), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle the relation-changed event. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(config_dict=self.config_dict): - return - event.relation.data[self.model.app].update( - (key, str(self.config_dict[key])) for key in self.config_dict - ) - - def update_config(self, config_dict: Dict) -> None: - """Allow for updates to relation. - - Args: - config_dict: Contains all the configuration options for Ingress. - - Attrs: - config_dict: Contains all the configuration options for Ingress. - """ - if self.model.unit.is_leader(): - self.config_dict = self._convert_to_relation_interface(config_dict) - if self._config_dict_errors(self.config_dict, update_only=True): - return - relation = self.model.get_relation(INGRESS_RELATION_NAME) - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressBaseProvides(Object): - """Parent class for IngressProvides and IngressProxyProvides. - - Attrs: - model: Juju model where the charm is deployed. - """ - - def __init__(self, charm: CharmBase, relation_name: str) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, relation_name) - self.charm = charm - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle a change to the ingress/ingress-proxy relation. - - Confirm we have the fields we expect to receive. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - relation_name = event.relation.name - - assert event.app is not None # nosec - if not event.relation.data[event.app]: - LOGGER.info( - "%s hasn't finished configuring, waiting until relation is changed again.", - relation_name, - ) - return - - ingress_data = { - field: event.relation.data[event.app].get(field) - for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - } - - missing_fields = sorted( - field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None - ) - - if missing_fields: - LOGGER.warning( - "Missing required data fields for %s relation: %s", - relation_name, - ", ".join(missing_fields), - ) - self.model.unit.status = BlockedStatus( - f"Missing fields for {relation_name}: {', '.join(missing_fields)}" - ) - - if relation_name == INGRESS_RELATION_NAME: - # Conform to charm-relation-interfaces. - if "name" in ingress_data and "port" in ingress_data: - name = ingress_data["name"] - port = ingress_data["port"] - else: - name = ingress_data["service-name"] - port = ingress_data["service-port"] - event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/" - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - elif relation_name == INGRESS_PROXY_RELATION_NAME: - self.charm.on.ingress_proxy_available.emit() - - -class IngressProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress' relation. - - Attrs: - charm: The charm that provides the ingress relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProvides class. - - Args: - charm: The charm that provides the ingress relation. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken - ) - - def _on_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle a relation-broken event in the ingress relation. - - Args: - event: Event triggering the relation-broken hook for the relation. - """ - if not self.model.unit.is_leader(): - return - - # Create an event that our charm can use to remove the ingress resource. - self.charm.on.ingress_broken.emit(event.relation) - - -class IngressProxyProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation. - - Attrs: - charm: The charm that provides the ingress-proxy relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, INGRESS_PROXY_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed - ) diff --git a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/heat-k8s/osci.yaml b/charms/heat-k8s/osci.yaml deleted file mode 100644 index a971062f..00000000 --- a/charms/heat-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: heat-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/heat-k8s/pyproject.toml b/charms/heat-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/heat-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/heat-k8s/rename.sh b/charms/heat-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/heat-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/heat-k8s/requirements.txt b/charms/heat-k8s/requirements.txt index 88f53cfa..9bf4ecff 100644 --- a/charms/heat-k8s/requirements.txt +++ b/charms/heat-k8s/requirements.txt @@ -1,7 +1,6 @@ ops jinja2 pwgen -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pydantic<2.0 @@ -9,3 +8,6 @@ pydantic<2.0 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # Charmhelpers is only present as interface_ceph_client uses it. git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/heat-k8s/src/templates/parts/section-database b/charms/heat-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/heat-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/heat-k8s/src/templates/parts/section-federation b/charms/heat-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/heat-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/heat-k8s/src/templates/parts/section-identity b/charms/heat-k8s/src/templates/parts/section-identity deleted file mode 100644 index d8b11646..00000000 --- a/charms/heat-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,38 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - - -[trustee] -auth_type = password -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -user_domain_name = {{ identity_service.service_domain_name }} diff --git a/charms/heat-k8s/src/templates/parts/section-middleware b/charms/heat-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/heat-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/heat-k8s/src/templates/parts/section-signing b/charms/heat-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/heat-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/heat-k8s/test-requirements.txt b/charms/heat-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/heat-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/heat-k8s/tests/unit/test_heat_charm.py b/charms/heat-k8s/tests/unit/test_heat_charm.py index 05652c02..d230cb1b 100644 --- a/charms/heat-k8s/tests/unit/test_heat_charm.py +++ b/charms/heat-k8s/tests/unit/test_heat_charm.py @@ -22,13 +22,12 @@ from unittest.mock import ( Mock, ) +import charm import ops_sunbeam.test_utils as test_utils from ops.testing import ( Harness, ) -import charm - class _HeatTestOperatorCharm(charm.HeatOperatorCharm): """Test Operator Charm for Heat Operator.""" diff --git a/charms/heat-k8s/tox.ini b/charms/heat-k8s/tox.ini deleted file mode 100644 index 848869a4..00000000 --- a/charms/heat-k8s/tox.ini +++ /dev/null @@ -1,170 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest - extras -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/horizon-k8s/.flake8 b/charms/horizon-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/horizon-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/horizon-k8s/.gitignore b/charms/horizon-k8s/.gitignore deleted file mode 100644 index 2b0b5a83..00000000 --- a/charms/horizon-k8s/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -venv/ -build/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] -.stestr/ diff --git a/charms/horizon-k8s/.gitreview b/charms/horizon-k8s/.gitreview deleted file mode 100644 index dbdc5803..00000000 --- a/charms/horizon-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-horizon-k8s.git -defaultbranch=main diff --git a/charms/horizon-k8s/.jujuignore b/charms/horizon-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/horizon-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/horizon-k8s/.stestr.conf b/charms/horizon-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/horizon-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/horizon-k8s/.zuul.yaml b/charms/horizon-k8s/.zuul.yaml deleted file mode 100644 index fd20909e..00000000 --- a/charms/horizon-k8s/.zuul.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs diff --git a/charms/horizon-k8s/charmcraft.yaml b/charms/horizon-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/horizon-k8s/charmcraft.yaml +++ b/charms/horizon-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/horizon-k8s/fetch-libs.sh b/charms/horizon-k8s/fetch-libs.sh deleted file mode 100755 index a276fddb..00000000 --- a/charms/horizon-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -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.v0.identity_credentials -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) 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 deleted file mode 100644 index 162a46a8..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,439 +0,0 @@ -"""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/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py deleted file mode 100644 index 9ff0a8d3..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py +++ /dev/null @@ -1,439 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_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.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_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.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials 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 = "a5d96cc2686c47eea554ce2210c2d24e" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - -logger = logging.getLogger(__name__) - - -class CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials 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._cloud_credentials_rel.data[self._cloud_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 CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients 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 CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials 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_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_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 cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_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/lib/charms/keystone_k8s/v1/identity_service.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c3fac4ca..00000000 --- a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Library for the ingress relation. - -This library contains the Requires and Provides classes for handling -the ingress interface. - -Import `IngressRequires` in your charm, with two required options: - - "self" (the charm itself) - - config_dict - -`config_dict` accepts the following keys: - - service-hostname (required) - - service-name (required) - - service-port (required) - - additional-hostnames - - limit-rps - - limit-whitelist - - max-body-size - - owasp-modsecurity-crs - - path-routes - - retry-errors - - rewrite-enabled - - rewrite-target - - service-namespace - - session-cookie-max-age - - tls-secret-name - -See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions -of each, along with the required type. - -As an example, add the following to `src/charm.py`: -``` -from charms.nginx_ingress_integrator.v0.ingress import IngressRequires - -# In your charm's `__init__` method. -self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"], - "service-name": self.app.name, - "service-port": 80}) - -# In your charm's `config-changed` handler. -self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) -``` -And then add the following to `metadata.yaml`: -``` -requires: - ingress: - interface: ingress -``` -You _must_ register the IngressRequires class as part of the `__init__` method -rather than, for instance, a config-changed event handler. This is because -doing so won't get the current relation changed event, because it wasn't -registered to handle the event (because it wasn't created in `__init__` when -the event was fired). -""" - -import logging - -from ops.charm import CharmEvents -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -# The unique Charmhub library identifier, never change it -LIBID = "db0af4367506491c91663468fb5caa4c" - -# 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 = 10 - -logger = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = { - "service-hostname", - "service-name", - "service-port", -} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - - -class IngressAvailableEvent(EventBase): - pass - - -class IngressBrokenEvent(EventBase): - pass - - -class IngressCharmEvents(CharmEvents): - """Custom charm events.""" - - ingress_available = EventSource(IngressAvailableEvent) - ingress_broken = EventSource(IngressBrokenEvent) - - -class IngressRequires(Object): - """This class defines the functionality for the 'requires' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm, config_dict): - super().__init__(charm, "ingress") - - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - - self.config_dict = config_dict - - def _config_dict_errors(self, update_only=False): - """Check our config dict for errors.""" - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - x - for x in self.config_dict - if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - ] - if unknown: - logger.error( - "Ingress relation error, unknown key(s) in config dictionary found: %s", - ", ".join(unknown), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - if not update_only: - missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict] - if missing: - logger.error( - "Ingress relation error, missing required key(s) in config dictionary: %s", - ", ".join(sorted(missing)), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - return False - - def _on_relation_changed(self, event): - """Handle the relation-changed event.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(): - return - for key in self.config_dict: - event.relation.data[self.model.app][key] = str(self.config_dict[key]) - - def update_config(self, config_dict): - """Allow for updates to relation.""" - if self.model.unit.is_leader(): - self.config_dict = config_dict - if self._config_dict_errors(update_only=True): - return - relation = self.model.get_relation("ingress") - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressProvides(Object): - """This class defines the functionality for the 'provides' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm): - super().__init__(charm, "ingress") - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken) - self.charm = charm - - def _on_relation_changed(self, event): - """Handle a change to the ingress relation. - - Confirm we have the fields we expect to receive.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - ingress_data = { - field: event.relation.data[event.app].get(field) - for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - } - - missing_fields = sorted( - [ - field - for field in REQUIRED_INGRESS_RELATION_FIELDS - if ingress_data.get(field) is None - ] - ) - - if missing_fields: - logger.error( - "Missing required data fields for ingress relation: {}".format( - ", ".join(missing_fields) - ) - ) - self.model.unit.status = BlockedStatus( - "Missing fields for ingress: {}".format(", ".join(missing_fields)) - ) - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - - def _on_relation_broken(self, _): - """Handle a relation-broken event in the ingress relation.""" - if not self.model.unit.is_leader(): - return - - # Create an event that our charm can use to remove the ingress resource. - self.charm.on.ingress_broken.emit() diff --git a/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/horizon-k8s/osci.yaml b/charms/horizon-k8s/osci.yaml deleted file mode 100644 index 3e23b3e6..00000000 --- a/charms/horizon-k8s/osci.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - charm-unit-jobs-py38 - - charm-unit-jobs-py310 - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: horizon-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/horizon-k8s/pyproject.toml b/charms/horizon-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/horizon-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/horizon-k8s/rename.sh b/charms/horizon-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/horizon-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/horizon-k8s/requirements.txt b/charms/horizon-k8s/requirements.txt index 11632a5d..64399ba9 100644 --- a/charms/horizon-k8s/requirements.txt +++ b/charms/horizon-k8s/requirements.txt @@ -11,4 +11,6 @@ pydantic<2.0 lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/horizon-k8s/src/templates/parts/database-connection b/charms/horizon-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/horizon-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/horizon-k8s/test-requirements.txt b/charms/horizon-k8s/test-requirements.txt deleted file mode 100644 index ebee5935..00000000 --- a/charms/horizon-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/horizon-k8s/tests/unit/test_horizon_charm.py b/charms/horizon-k8s/tests/unit/test_horizon_charm.py index 48be6733..95a041a3 100644 --- a/charms/horizon-k8s/tests/unit/test_horizon_charm.py +++ b/charms/horizon-k8s/tests/unit/test_horizon_charm.py @@ -16,11 +16,10 @@ """Unit tests for Horizon operator.""" +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _HorizonOperatorCharm(charm.HorizonOperatorCharm): """Test Operator Charm for Horizon Operator.""" diff --git a/charms/horizon-k8s/tox.ini b/charms/horizon-k8s/tox.ini deleted file mode 100644 index 671cda90..00000000 --- a/charms/horizon-k8s/tox.ini +++ /dev/null @@ -1,132 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/keystone-k8s/.flake8 b/charms/keystone-k8s/.flake8 deleted file mode 100644 index c0a92a06..00000000 --- a/charms/keystone-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 80 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/keystone-k8s/.gitignore b/charms/keystone-k8s/.gitignore deleted file mode 100644 index 4df34f6a..00000000 --- a/charms/keystone-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -.idea/ -*.charm -.tox -venv -.coverage -__pycache__/ -*.py[cod] -**.swp -.stestr/ diff --git a/charms/keystone-k8s/.gitreview b/charms/keystone-k8s/.gitreview deleted file mode 100644 index ba705e1e..00000000 --- a/charms/keystone-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-keystone-k8s.git -defaultbranch=main diff --git a/charms/keystone-k8s/.stestr.conf b/charms/keystone-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/keystone-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/keystone-k8s/.zuul.yaml b/charms/keystone-k8s/.zuul.yaml deleted file mode 100644 index bfda2e24..00000000 --- a/charms/keystone-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: keystone-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/keystone-k8s/charmcraft.yaml b/charms/keystone-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/keystone-k8s/charmcraft.yaml +++ b/charms/keystone-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/keystone-k8s/fetch-libs.sh b/charms/keystone-k8s/fetch-libs.sh deleted file mode 100755 index a49af7ed..00000000 --- a/charms/keystone-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -#charmcraft fetch-lib charms.sunbeam_keystone_operator.v1.identity_service -#charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_credentials -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py deleted file mode 100644 index e3f4565d..00000000 --- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,458 +0,0 @@ -"""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 = 3 - -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') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - 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, - admin_role: 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 - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint - app_data["admin-role"] = admin_role diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 1f10383a..00000000 --- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,393 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": <request id> - "tag": <string to identify request> - "ops": [ - { - "name": <op name>, - "params": { - <param 1>: <value 1>, - <param 2>: <value 2> - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 4 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request") - if request is not None: - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c3fac4ca..00000000 --- a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Library for the ingress relation. - -This library contains the Requires and Provides classes for handling -the ingress interface. - -Import `IngressRequires` in your charm, with two required options: - - "self" (the charm itself) - - config_dict - -`config_dict` accepts the following keys: - - service-hostname (required) - - service-name (required) - - service-port (required) - - additional-hostnames - - limit-rps - - limit-whitelist - - max-body-size - - owasp-modsecurity-crs - - path-routes - - retry-errors - - rewrite-enabled - - rewrite-target - - service-namespace - - session-cookie-max-age - - tls-secret-name - -See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions -of each, along with the required type. - -As an example, add the following to `src/charm.py`: -``` -from charms.nginx_ingress_integrator.v0.ingress import IngressRequires - -# In your charm's `__init__` method. -self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"], - "service-name": self.app.name, - "service-port": 80}) - -# In your charm's `config-changed` handler. -self.ingress.update_config({"service-hostname": self.config["external_hostname"]}) -``` -And then add the following to `metadata.yaml`: -``` -requires: - ingress: - interface: ingress -``` -You _must_ register the IngressRequires class as part of the `__init__` method -rather than, for instance, a config-changed event handler. This is because -doing so won't get the current relation changed event, because it wasn't -registered to handle the event (because it wasn't created in `__init__` when -the event was fired). -""" - -import logging - -from ops.charm import CharmEvents -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -# The unique Charmhub library identifier, never change it -LIBID = "db0af4367506491c91663468fb5caa4c" - -# 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 = 10 - -logger = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = { - "service-hostname", - "service-name", - "service-port", -} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - - -class IngressAvailableEvent(EventBase): - pass - - -class IngressBrokenEvent(EventBase): - pass - - -class IngressCharmEvents(CharmEvents): - """Custom charm events.""" - - ingress_available = EventSource(IngressAvailableEvent) - ingress_broken = EventSource(IngressBrokenEvent) - - -class IngressRequires(Object): - """This class defines the functionality for the 'requires' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm, config_dict): - super().__init__(charm, "ingress") - - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - - self.config_dict = config_dict - - def _config_dict_errors(self, update_only=False): - """Check our config dict for errors.""" - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - x - for x in self.config_dict - if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - ] - if unknown: - logger.error( - "Ingress relation error, unknown key(s) in config dictionary found: %s", - ", ".join(unknown), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - if not update_only: - missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict] - if missing: - logger.error( - "Ingress relation error, missing required key(s) in config dictionary: %s", - ", ".join(sorted(missing)), - ) - self.model.unit.status = BlockedStatus(blocked_message) - return True - return False - - def _on_relation_changed(self, event): - """Handle the relation-changed event.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(): - return - for key in self.config_dict: - event.relation.data[self.model.app][key] = str(self.config_dict[key]) - - def update_config(self, config_dict): - """Allow for updates to relation.""" - if self.model.unit.is_leader(): - self.config_dict = config_dict - if self._config_dict_errors(update_only=True): - return - relation = self.model.get_relation("ingress") - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressProvides(Object): - """This class defines the functionality for the 'provides' side of the 'ingress' relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm): - super().__init__(charm, "ingress") - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed) - self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken) - self.charm = charm - - def _on_relation_changed(self, event): - """Handle a change to the ingress relation. - - Confirm we have the fields we expect to receive.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - ingress_data = { - field: event.relation.data[event.app].get(field) - for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS - } - - missing_fields = sorted( - [ - field - for field in REQUIRED_INGRESS_RELATION_FIELDS - if ingress_data.get(field) is None - ] - ) - - if missing_fields: - logger.error( - "Missing required data fields for ingress relation: {}".format( - ", ".join(missing_fields) - ) - ) - self.model.unit.status = BlockedStatus( - "Missing fields for ingress: {}".format(", ".join(missing_fields)) - ) - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - - def _on_relation_broken(self, _): - """Handle a relation-broken event in the ingress relation.""" - if not self.model.unit.is_leader(): - return - - # Create an event that our charm can use to remove the ingress resource. - self.charm.on.ingress_broken.emit() diff --git a/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/keystone-k8s/osci.yaml b/charms/keystone-k8s/osci.yaml deleted file mode 100644 index 0d6d4c1c..00000000 --- a/charms/keystone-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: keystone-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/keystone-k8s/pyproject.toml b/charms/keystone-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/keystone-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/keystone-k8s/rename.sh b/charms/keystone-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/keystone-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/keystone-k8s/requirements.txt b/charms/keystone-k8s/requirements.txt index 1bec875c..e9b413e7 100644 --- a/charms/keystone-k8s/requirements.txt +++ b/charms/keystone-k8s/requirements.txt @@ -13,6 +13,7 @@ lightkube-models ops pwgen -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - python-keystoneclient # keystone-k8s + +# From ops_sunbeam +tenacity diff --git a/charms/keystone-k8s/src/charm.py b/charms/keystone-k8s/src/charm.py index 2f0d0628..9a65ddc2 100755 --- a/charms/keystone-k8s/src/charm.py +++ b/charms/keystone-k8s/src/charm.py @@ -71,7 +71,6 @@ from ops.model import ( SecretNotFoundError, SecretRotate, ) - from utils import ( manager, ) diff --git a/charms/keystone-k8s/src/templates/parts/section-database b/charms/keystone-k8s/src/templates/parts/section-database deleted file mode 100644 index 88b0abd5..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/keystone/keystone.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/keystone-k8s/src/templates/parts/section-federation b/charms/keystone-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/keystone-k8s/src/templates/parts/section-middleware b/charms/keystone-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications b/charms/keystone-k8s/src/templates/parts/section-oslo-notifications deleted file mode 100644 index ce559feb..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications +++ /dev/null @@ -1,4 +0,0 @@ -{% if options.enable_telemetry_notifications -%} -[oslo_messaging_notifications] -driver = messagingv2 -{%- endif %} diff --git a/charms/keystone-k8s/src/templates/parts/section-signing b/charms/keystone-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/keystone-k8s/src/utils/manager.py b/charms/keystone-k8s/src/utils/manager.py index 182ae147..91d282f0 100644 --- a/charms/keystone-k8s/src/utils/manager.py +++ b/charms/keystone-k8s/src/utils/manager.py @@ -37,7 +37,6 @@ from ops import ( from ops.model import ( MaintenanceStatus, ) - from utils.client import ( KeystoneClient, KeystoneExceptionError, diff --git a/charms/keystone-k8s/test-requirements.txt b/charms/keystone-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/keystone-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/keystone-k8s/tests/unit/test_keystone_charm.py b/charms/keystone-k8s/tests/unit/test_keystone_charm.py index 37af2812..96c76ebb 100644 --- a/charms/keystone-k8s/tests/unit/test_keystone_charm.py +++ b/charms/keystone-k8s/tests/unit/test_keystone_charm.py @@ -24,11 +24,10 @@ from unittest.mock import ( MagicMock, ) +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _KeystoneOperatorCharm(charm.KeystoneOperatorCharm): """Create Keystone operator test charm.""" diff --git a/charms/keystone-k8s/tox.ini b/charms/keystone-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/keystone-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/keystone-ldap-k8s/.gitignore b/charms/keystone-ldap-k8s/.gitignore deleted file mode 100644 index 4df34f6a..00000000 --- a/charms/keystone-ldap-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -.idea/ -*.charm -.tox -venv -.coverage -__pycache__/ -*.py[cod] -**.swp -.stestr/ diff --git a/charms/keystone-ldap-k8s/.gitreview b/charms/keystone-ldap-k8s/.gitreview deleted file mode 100644 index 069407b6..00000000 --- a/charms/keystone-ldap-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-keystone-ldap-k8s.git -defaultbranch=main diff --git a/charms/keystone-ldap-k8s/.stestr.conf b/charms/keystone-ldap-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/keystone-ldap-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/keystone-ldap-k8s/.zuul.yaml b/charms/keystone-ldap-k8s/.zuul.yaml deleted file mode 100644 index 17366a2c..00000000 --- a/charms/keystone-ldap-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: keystone-ldap-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/keystone-ldap-k8s/charmcraft.yaml b/charms/keystone-ldap-k8s/charmcraft.yaml index 8c3cfc6f..12fc2d53 100644 --- a/charms/keystone-ldap-k8s/charmcraft.yaml +++ b/charms/keystone-ldap-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py deleted file mode 100644 index 0d9858c9..00000000 --- a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Interface for passing domain configuration.""" - -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) -import base64 -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "dfeee73ed0b248c29ed905aeda6fd417" - -# 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 - -class DomainConfigRequestEvent(RelationEvent): - """DomainConfigRequest Event.""" - pass - -class DomainConfigProviderEvents(ObjectEvents): - """Events class for `on`.""" - - remote_ready = EventSource(DomainConfigRequestEvent) - -class DomainConfigProvides(Object): - """DomainConfigProvides class.""" - - on = DomainConfigProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_domain_config_relation_changed, - ) - - def _on_domain_config_relation_changed( - self, event: RelationChangedEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig relation changed") - self.on.remote_ready.emit(event.relation) - - def set_domain_info( - self, domain_name: str, config_contents: str, ca=None - ) -> None: - """Set ceilometer configuration on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping set config") - return - for relation in self.relations: - relation.data[self.charm.app]["domain-name"] = domain_name - relation.data[self.charm.app]["config-contents"] = base64.b64encode(config_contents.encode()).decode() - if ca: - relation.data[self.charm.app]["ca"] = base64.b64encode(ca.encode()).decode() - - @property - def relations(self): - return self.framework.model.relations[self.relation_name] - -class DomainConfigChangedEvent(RelationEvent): - """DomainConfigChanged Event.""" - - pass - - -class DomainConfigGoneAwayEvent(RelationBrokenEvent): - """DomainConfigGoneAway Event.""" - - pass - - -class DomainConfigRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - config_changed = EventSource(DomainConfigChangedEvent) - goneaway = EventSource(DomainConfigGoneAwayEvent) - - -class DomainConfigRequires(Object): - """DomainConfigRequires class.""" - - on = DomainConfigRequirerEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_domain_config_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_domain_config_relation_broken, - ) - - def _on_domain_config_relation_changed( - self, event: RelationChangedEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig config data changed") - self.on.config_changed.emit(event.relation) - - def _on_domain_config_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig on_broken") - self.on.goneaway.emit(event.relation) - - def get_domain_configs(self, exclude=None): - exclude = exclude or [] - configs = [] - for relation in self.relations: - if relation in exclude: - continue - try: - domain_name = relation.data[relation.app].get("domain-name") - except KeyError: - logging.debug("Key error accessing app data") - continue - raw_config_contents = relation.data[relation.app].get("config-contents") - if not all([domain_name, raw_config_contents]): - continue - raw_ca = relation.data[relation.app].get("ca") - config = { - "domain-name": domain_name, - "config-contents": base64.b64decode(raw_config_contents).decode()} - if raw_ca: - config["ca"] = base64.b64decode(raw_ca).decode() - configs.append(config) - return configs - - @property - def relations(self): - return self.framework.model.relations[self.relation_name] - diff --git a/charms/keystone-ldap-k8s/osci.yaml b/charms/keystone-ldap-k8s/osci.yaml deleted file mode 100644 index c5faf352..00000000 --- a/charms/keystone-ldap-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: keystone-ldap-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/keystone-ldap-k8s/pyproject.toml b/charms/keystone-ldap-k8s/pyproject.toml deleted file mode 100644 index 2edc519a..00000000 --- a/charms/keystone-ldap-k8s/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 99 -target-version = ["py38"] - -[tool.isort] -line_length = 99 -profile = "black" - -# Linting tools configuration -[tool.flake8] -max-line-length = 99 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107"] -# D100, D101, D102, D103: Ignore missing docstrings in tests -per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] -docstring-convention = "google" diff --git a/charms/keystone-ldap-k8s/rename.sh b/charms/keystone-ldap-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/keystone-ldap-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/keystone-ldap-k8s/requirements.txt b/charms/keystone-ldap-k8s/requirements.txt index d4f4a3d0..f545ded9 100644 --- a/charms/keystone-ldap-k8s/requirements.txt +++ b/charms/keystone-ldap-k8s/requirements.txt @@ -12,6 +12,7 @@ lightkube-models ops pwgen -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - python-keystoneclient # keystone-k8s + +# From ops_sunbeam +tenacity diff --git a/charms/keystone-ldap-k8s/src/charm.py b/charms/keystone-ldap-k8s/src/charm.py index fc4c9ede..26d3ae74 100755 --- a/charms/keystone-ldap-k8s/src/charm.py +++ b/charms/keystone-ldap-k8s/src/charm.py @@ -23,7 +23,11 @@ Send domain configuration to the keystone charm. """ import json import logging -from typing import Callable, List, Mapping +from typing import ( + Callable, + List, + Mapping, +) import charms.keystone_k8s.v0.domain_config as sunbeam_dc_svc import jinja2 @@ -31,7 +35,9 @@ import ops.charm import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as config_contexts import ops_sunbeam.relation_handlers as sunbeam_rhandlers -from ops.main import main +from ops.main import ( + main, +) # Log messages can be retrieved using juju debug-log logger = logging.getLogger(__name__) @@ -94,7 +100,9 @@ class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm): def __init__(self, *args): super().__init__(*args) - def get_relation_handlers(self, handlers=None) -> List[sunbeam_rhandlers.RelationHandler]: + def get_relation_handlers( + self, handlers=None + ) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] if self.can_add_handler(self.DOMAIN_CONFIG_RELATION_NAME, handlers): diff --git a/charms/keystone-ldap-k8s/test-requirements.txt b/charms/keystone-ldap-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/keystone-ldap-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/keystone-ldap-k8s/tests/integration/test_charm.py b/charms/keystone-ldap-k8s/tests/integration/test_charm.py index 18f24d39..3911a065 100644 --- a/charms/keystone-ldap-k8s/tests/integration/test_charm.py +++ b/charms/keystone-ldap-k8s/tests/integration/test_charm.py @@ -1,14 +1,32 @@ #!/usr/bin/env python3 -# Copyright 2023 liam -# See LICENSE file for licensing details. + +# Copyright 2021 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Define keystone-ldap integration tests.""" import asyncio import logging -from pathlib import Path +from pathlib import ( + Path, +) import pytest import yaml -from pytest_operator.plugin import OpsTest +from pytest_operator.plugin import ( + OpsTest, +) logger = logging.getLogger(__name__) @@ -24,12 +42,21 @@ async def test_build_and_deploy(ops_test: OpsTest): """ # Build and deploy charm from local source folder charm = await ops_test.build_charm(".") - resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} + resources = { + "httpbin-image": METADATA["resources"]["httpbin-image"][ + "upstream-source" + ] + } # Deploy the charm and wait for active/idle status await asyncio.gather( - ops_test.model.deploy(await charm, resources=resources, application_name=APP_NAME), + ops_test.model.deploy( + await charm, resources=resources, application_name=APP_NAME + ), ops_test.model.wait_for_idle( - apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 + apps=[APP_NAME], + status="active", + raise_on_blocked=True, + timeout=1000, ), ) diff --git a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py index 07c6a02d..3d0cf6cb 100644 --- a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py +++ b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py @@ -19,10 +19,11 @@ import base64 import json -import ops_sunbeam.test_utils as test_utils -from ops.testing import Harness - import charm +import ops_sunbeam.test_utils as test_utils +from ops.testing import ( + Harness, +) class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm): @@ -45,6 +46,8 @@ class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm): class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase): + """Create Keystone Ldap test charm.""" + def setUp(self): """Run test setup.""" self.harness = Harness(charm.KeystoneLDAPK8SCharm) @@ -56,7 +59,9 @@ class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase): self.harness.set_leader() rel_id = self.harness.add_relation("domain-config", "keystone") self.harness.add_relation_unit(rel_id, "keystone/0") - rel_data = self.harness.get_relation_data(rel_id, self.harness.charm.unit.app.name) + rel_data = self.harness.get_relation_data( + rel_id, self.harness.charm.unit.app.name + ) ldap_config_flags = json.dumps( { "group_tree_dn": "ou=groups,dc=test,dc=com", diff --git a/charms/keystone-ldap-k8s/tox.ini b/charms/keystone-ldap-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/keystone-ldap-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/magnum-k8s/.flake8 b/charms/magnum-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/magnum-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/magnum-k8s/.gitignore b/charms/magnum-k8s/.gitignore deleted file mode 100644 index 33d25ac9..00000000 --- a/charms/magnum-k8s/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -.stestr/ diff --git a/charms/magnum-k8s/.gitreview b/charms/magnum-k8s/.gitreview deleted file mode 100644 index 6bdf9404..00000000 --- a/charms/magnum-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-magnum-k8s.git -defaultbranch=main diff --git a/charms/magnum-k8s/.jujuignore b/charms/magnum-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/magnum-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/magnum-k8s/.stestr.conf b/charms/magnum-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/magnum-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/magnum-k8s/.zuul.yaml b/charms/magnum-k8s/.zuul.yaml deleted file mode 100644 index 5509ca5f..00000000 --- a/charms/magnum-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: magnum-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/magnum-k8s/charmcraft.yaml b/charms/magnum-k8s/charmcraft.yaml index 3a9d1b7d..ea13253e 100644 --- a/charms/magnum-k8s/charmcraft.yaml +++ b/charms/magnum-k8s/charmcraft.yaml @@ -17,4 +17,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/magnum-k8s/fetch-libs.sh b/charms/magnum-k8s/fetch-libs.sh deleted file mode 100755 index a573dd64..00000000 --- a/charms/magnum-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -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.v0.identity_resource -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 6ef944ef..00000000 --- a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,373 +0,0 @@ -"""IdentityResourceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": <request id> - "tag": <string to identify request> - "ops": [ - { - "name": <op name>, - "params": { - <param 1>: <value 1>, - <param 2>: <value 2> - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging - -from ops.charm import ( - RelationEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 2 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined(self, event): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed(self, event): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken(self, event): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Relation: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store(self, id: str, tag: str, ops: list, state: int): - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - 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_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed(self, event): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ): - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/magnum-k8s/osci.yaml b/charms/magnum-k8s/osci.yaml deleted file mode 100644 index c5db11f6..00000000 --- a/charms/magnum-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: magnum-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/magnum-k8s/pyproject.toml b/charms/magnum-k8s/pyproject.toml deleted file mode 100644 index e6de53b4..00000000 --- a/charms/magnum-k8s/pyproject.toml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" - diff --git a/charms/magnum-k8s/rename.sh b/charms/magnum-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/magnum-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/magnum-k8s/requirements.txt b/charms/magnum-k8s/requirements.txt index 1dfcf975..20492790 100644 --- a/charms/magnum-k8s/requirements.txt +++ b/charms/magnum-k8s/requirements.txt @@ -1,6 +1,8 @@ ops jinja2 pydantic<2.0 -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pwgen + +# From ops_sunbeam +tenacity diff --git a/charms/magnum-k8s/src/templates/magnum.conf.j2 b/charms/magnum-k8s/src/templates/magnum.conf.j2 index 6ef7b2ba..aa67c6da 100644 --- a/charms/magnum-k8s/src/templates/magnum.conf.j2 +++ b/charms/magnum-k8s/src/templates/magnum.conf.j2 @@ -5,11 +5,16 @@ state_path = /var/lib/magnum transport_url = {{ amqp.transport_url }} -{% include "parts/database-connection" %} +{% if database.connection -%} +sql_connection = {{ database.connection }} +{% endif -%} db_auto_create = false {% include "parts/section-identity" %} +[keystone_auth] +{% include "parts/identity-data" %} + {% include "parts/section-service-user" %} {% include "parts/section-trust" %} diff --git a/charms/magnum-k8s/src/templates/parts/database-connection b/charms/magnum-k8s/src/templates/parts/database-connection deleted file mode 100644 index 058d99ad..00000000 --- a/charms/magnum-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -sql_connection = {{ database.connection }} -{% endif -%} diff --git a/charms/magnum-k8s/src/templates/parts/identity-data b/charms/magnum-k8s/src/templates/parts/identity-data deleted file mode 100644 index db28064a..00000000 --- a/charms/magnum-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,26 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - -# XXX Region should come from the id relation here -region_name = {{ options.region }} diff --git a/charms/magnum-k8s/src/templates/parts/section-database b/charms/magnum-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/magnum-k8s/src/templates/parts/section-identity b/charms/magnum-k8s/src/templates/parts/section-identity deleted file mode 100644 index b45ab19a..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,5 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} - -[keystone_auth] -{% include "parts/identity-data" %} diff --git a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/magnum-k8s/src/templates/parts/section-service-user b/charms/magnum-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/magnum-k8s/test-requirements.txt b/charms/magnum-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/magnum-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/magnum-k8s/tests/unit/test_magnum_charm.py b/charms/magnum-k8s/tests/unit/test_magnum_charm.py index 6466f9a4..c157e546 100644 --- a/charms/magnum-k8s/tests/unit/test_magnum_charm.py +++ b/charms/magnum-k8s/tests/unit/test_magnum_charm.py @@ -18,6 +18,7 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( Mock, @@ -26,8 +27,6 @@ from ops.testing import ( Harness, ) -import charm - class _MagnumTestOperatorCharm(charm.MagnumOperatorCharm): """Test Operator Charm for Magnum Operator.""" diff --git a/charms/magnum-k8s/tox.ini b/charms/magnum-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/magnum-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/neutron-k8s/.flake8 b/charms/neutron-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/neutron-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/neutron-k8s/.gitignore b/charms/neutron-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/neutron-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/neutron-k8s/.gitreview b/charms/neutron-k8s/.gitreview deleted file mode 100644 index bd60210f..00000000 --- a/charms/neutron-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-neutron-k8s.git -defaultbranch=main diff --git a/charms/neutron-k8s/.jujuignore b/charms/neutron-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/neutron-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/neutron-k8s/.stestr.conf b/charms/neutron-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/neutron-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/neutron-k8s/.zuul.yaml b/charms/neutron-k8s/.zuul.yaml deleted file mode 100644 index 710da9f9..00000000 --- a/charms/neutron-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: neutron-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/neutron-k8s/charmcraft.yaml b/charms/neutron-k8s/charmcraft.yaml index 5ac17253..fa60d165 100644 --- a/charms/neutron-k8s/charmcraft.yaml +++ b/charms/neutron-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/neutron-k8s/fetch-libs.sh b/charms/neutron-k8s/fetch-libs.sh deleted file mode 100755 index 4f3bd45d..00000000 --- a/charms/neutron-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/neutron-k8s/osci.yaml b/charms/neutron-k8s/osci.yaml deleted file mode 100644 index dba34983..00000000 --- a/charms/neutron-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: neutron-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/neutron-k8s/pyproject.toml b/charms/neutron-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/neutron-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/neutron-k8s/rename.sh b/charms/neutron-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/neutron-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/neutron-k8s/requirements.txt b/charms/neutron-k8s/requirements.txt index e988c61d..64399ba9 100644 --- a/charms/neutron-k8s/requirements.txt +++ b/charms/neutron-k8s/requirements.txt @@ -12,4 +12,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/neutron-k8s/src/templates/parts/section-database b/charms/neutron-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/neutron-k8s/src/templates/parts/section-federation b/charms/neutron-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/neutron-k8s/src/templates/parts/section-identity b/charms/neutron-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/neutron-k8s/src/templates/parts/section-middleware b/charms/neutron-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/neutron-k8s/src/templates/parts/section-service-user b/charms/neutron-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/neutron-k8s/src/templates/parts/section-signing b/charms/neutron-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/neutron-k8s/test-requirements.txt b/charms/neutron-k8s/test-requirements.txt deleted file mode 100644 index c533b127..00000000 --- a/charms/neutron-k8s/test-requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/neutron-k8s/tests/unit/test_neutron_charm.py b/charms/neutron-k8s/tests/unit/test_neutron_charm.py index 606d0f09..ee850769 100644 --- a/charms/neutron-k8s/tests/unit/test_neutron_charm.py +++ b/charms/neutron-k8s/tests/unit/test_neutron_charm.py @@ -16,9 +16,8 @@ """Tests for neutron charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _NeutronOVNOperatorCharm(charm.NeutronOVNOperatorCharm): diff --git a/charms/neutron-k8s/tox.ini b/charms/neutron-k8s/tox.ini deleted file mode 100644 index fbaa02c5..00000000 --- a/charms/neutron-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/nova-k8s/.flake8 b/charms/nova-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/nova-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/nova-k8s/.gitignore b/charms/nova-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/nova-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/nova-k8s/.gitreview b/charms/nova-k8s/.gitreview deleted file mode 100644 index 1f46f0d9..00000000 --- a/charms/nova-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-nova-k8s.git -defaultbranch=main diff --git a/charms/nova-k8s/.jujuignore b/charms/nova-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/nova-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/nova-k8s/.stestr.conf b/charms/nova-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/nova-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/nova-k8s/.zuul.yaml b/charms/nova-k8s/.zuul.yaml deleted file mode 100644 index 619607cb..00000000 --- a/charms/nova-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: nova-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/nova-k8s/charmcraft.yaml b/charms/nova-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/nova-k8s/charmcraft.yaml +++ b/charms/nova-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/nova-k8s/fetch-libs.sh b/charms/nova-k8s/fetch-libs.sh deleted file mode 100755 index 896bde54..00000000 --- a/charms/nova-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.nova_compute_k8s.v0.cloud_compute diff --git a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/nova-k8s/osci.yaml b/charms/nova-k8s/osci.yaml deleted file mode 100644 index d707a693..00000000 --- a/charms/nova-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: nova-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/nova-k8s/pyproject.toml b/charms/nova-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/nova-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/nova-k8s/rename.sh b/charms/nova-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/nova-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/nova-k8s/requirements.txt b/charms/nova-k8s/requirements.txt index 408e0a2d..efcc785e 100644 --- a/charms/nova-k8s/requirements.txt +++ b/charms/nova-k8s/requirements.txt @@ -12,4 +12,6 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/nova-k8s/src/templates/nova.conf.j2 b/charms/nova-k8s/src/templates/nova.conf.j2 index 8f49f109..a42983fa 100644 --- a/charms/nova-k8s/src/templates/nova.conf.j2 +++ b/charms/nova-k8s/src/templates/nova.conf.j2 @@ -13,12 +13,7 @@ connection = sqlite:////var/lib/nova/nova_api.sqlite {% endif -%} connection_recycle_time = 200 -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/nova/nova.sqlite -{% endif -%} +{% include "parts/section-database" %} [glance] service_type = image @@ -26,14 +21,16 @@ service_name = glance valid_interfaces = admin region_name = {{ options.region }} -[keystone_authtoken] {% include "parts/section-identity" %} +region_name = {{ options.region }} [neutron] -{% include "parts/section-identity" %} +{% include "parts/identity-data" %} +region_name = {{ options.region }} [placement] -{% include "parts/section-identity" %} +{% include "parts/identity-data" %} +region_name = {{ options.region }} {% include "parts/section-service-user" %} diff --git a/charms/nova-k8s/src/templates/parts/section-identity b/charms/nova-k8s/src/templates/parts/section-identity deleted file mode 100644 index 5a48d675..00000000 --- a/charms/nova-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,25 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -# XXX Region should come from the id relation here -region_name = {{ options.region }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/nova-k8s/src/templates/parts/section-service-user b/charms/nova-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/nova-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/nova-k8s/test-requirements.txt b/charms/nova-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/nova-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/nova-k8s/tests/unit/test_nova_charm.py b/charms/nova-k8s/tests/unit/test_nova_charm.py index c9adf611..a9285295 100644 --- a/charms/nova-k8s/tests/unit/test_nova_charm.py +++ b/charms/nova-k8s/tests/unit/test_nova_charm.py @@ -16,9 +16,8 @@ """Unit tests for Nova operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _NovaTestOperatorCharm(charm.NovaOperatorCharm): diff --git a/charms/nova-k8s/tox.ini b/charms/nova-k8s/tox.ini deleted file mode 100644 index f6fb644e..00000000 --- a/charms/nova-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;update-status-hook-interval=1m - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/octavia-k8s/.gitignore b/charms/octavia-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/octavia-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/octavia-k8s/.gitreview b/charms/octavia-k8s/.gitreview deleted file mode 100644 index 26336e73..00000000 --- a/charms/octavia-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-octavia-k8s.git -defaultbranch=main diff --git a/charms/octavia-k8s/.stestr.conf b/charms/octavia-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/octavia-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/octavia-k8s/.zuul.yaml b/charms/octavia-k8s/.zuul.yaml deleted file mode 100644 index fc2ae5c1..00000000 --- a/charms/octavia-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: octavia-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/octavia-k8s/charmcraft.yaml b/charms/octavia-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/octavia-k8s/charmcraft.yaml +++ b/charms/octavia-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/octavia-k8s/fetch-libs.sh b/charms/octavia-k8s/fetch-libs.sh deleted file mode 100755 index fbd3ab38..00000000 --- a/charms/octavia-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 154fab83..00000000 --- a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,392 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": <request id> - "tag": <string to identify request> - "ops": [ - { - "name": <op name>, - "params": { - <param 1>: <value 1>, - <param 2>: <value 2> - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 3 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# 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 = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index be171d8e..00000000 --- a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1360 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - CertificateRevokedEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revoked - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - replicas_relation.data[self.app].pop("certificate") - replicas_relation.data[self.app].pop("ca") - replicas_relation.data[self.app].pop("chain") - self.unit.status = WaitingStatus("Waiting for new certificate") - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 12 - - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "examples": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - }, - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - "revoked": True, - } - ] - }, - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - "revoked": { - "$id": "#/properties/certificates/items/revoked", - "type": "boolean", - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateRevokedEvent(EventBase): - """Charm Event triggered when a TLS certificate is revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - revoked: bool, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - self.revoked = revoked - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - "revoked": self.revoked, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - self.revoked = snapshot["revoked"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: Optional[List[str]] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: Optional[str] = None, - email_address: Optional[str] = None, - country_name: Optional[str] = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - certificate_revoked = EventSource(CertificateRevokedEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: Optional[str] = None, - certificate_signing_request: Optional[str] = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", [])) - for certificate in provider_certificates: - certificate["revoked"] = True - relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggered on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - if certificate.get("revoked", False): - self.on.certificate_revoked.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - revoked=True, - ) - else: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/octavia-k8s/osci.yaml b/charms/octavia-k8s/osci.yaml deleted file mode 100644 index 0cb2ddd6..00000000 --- a/charms/octavia-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: octavia-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/octavia-k8s/pyproject.toml b/charms/octavia-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/octavia-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/octavia-k8s/rename.sh b/charms/octavia-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/octavia-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/octavia-k8s/requirements.txt b/charms/octavia-k8s/requirements.txt index 230257ac..db47310f 100644 --- a/charms/octavia-k8s/requirements.txt +++ b/charms/octavia-k8s/requirements.txt @@ -3,5 +3,7 @@ ops jinja2 jsonschema pydantic<2.0 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube + +# From ops_sunbeam +tenacity diff --git a/charms/octavia-k8s/src/templates/parts/database-connection b/charms/octavia-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/octavia-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/octavia-k8s/src/templates/parts/identity-data b/charms/octavia-k8s/src/templates/parts/identity-data deleted file mode 100644 index 574c3248..00000000 --- a/charms/octavia-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,21 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} diff --git a/charms/octavia-k8s/src/templates/parts/section-database b/charms/octavia-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/octavia-k8s/src/templates/parts/section-federation b/charms/octavia-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/octavia-k8s/src/templates/parts/section-identity b/charms/octavia-k8s/src/templates/parts/section-identity deleted file mode 100644 index 2cf792eb..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,3 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} -{% include "parts/service-token" %} diff --git a/charms/octavia-k8s/src/templates/parts/section-middleware b/charms/octavia-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/octavia-k8s/src/templates/parts/section-signing b/charms/octavia-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/octavia-k8s/src/templates/parts/service-token b/charms/octavia-k8s/src/templates/parts/service-token deleted file mode 100644 index 51c0073f..00000000 --- a/charms/octavia-k8s/src/templates/parts/service-token +++ /dev/null @@ -1,2 +0,0 @@ -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/octavia-k8s/test-requirements.txt b/charms/octavia-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/octavia-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/octavia-k8s/tests/unit/test_charm.py b/charms/octavia-k8s/tests/unit/test_charm.py index e84092d4..a4b7f6e1 100644 --- a/charms/octavia-k8s/tests/unit/test_charm.py +++ b/charms/octavia-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Unit tests for Octavia operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _OctaviaOVNOperatorCharm(charm.OctaviaOVNOperatorCharm): diff --git a/charms/octavia-k8s/tox.ini b/charms/octavia-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/octavia-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/openstack-exporter-k8s/.gitignore b/charms/openstack-exporter-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/openstack-exporter-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/openstack-exporter-k8s/.gitreview b/charms/openstack-exporter-k8s/.gitreview deleted file mode 100644 index 9859257a..00000000 --- a/charms/openstack-exporter-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-openstack-exporter-k8s.git -defaultbranch=main diff --git a/charms/openstack-exporter-k8s/.stestr.conf b/charms/openstack-exporter-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/openstack-exporter-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/openstack-exporter-k8s/.zuul.yaml b/charms/openstack-exporter-k8s/.zuul.yaml deleted file mode 100644 index 5748d306..00000000 --- a/charms/openstack-exporter-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: openstack-exporter-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/openstack-exporter-k8s/charmcraft.yaml b/charms/openstack-exporter-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/openstack-exporter-k8s/charmcraft.yaml +++ b/charms/openstack-exporter-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 1f10383a..00000000 --- a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,393 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": <request id> - "tag": <string to identify request> - "ops": [ - { - "name": <op name>, - "params": { - <param 1>: <value 1>, - <param 2>: <value 2> - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 4 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request") - if request is not None: - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index be171d8e..00000000 --- a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1360 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - CertificateRevokedEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revoked - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - replicas_relation.data[self.app].pop("certificate") - replicas_relation.data[self.app].pop("ca") - replicas_relation.data[self.app].pop("chain") - self.unit.status = WaitingStatus("Waiting for new certificate") - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 12 - - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "examples": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - }, - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - "revoked": True, - } - ] - }, - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - "revoked": { - "$id": "#/properties/certificates/items/revoked", - "type": "boolean", - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateRevokedEvent(EventBase): - """Charm Event triggered when a TLS certificate is revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - revoked: bool, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - self.revoked = revoked - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - "revoked": self.revoked, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - self.revoked = snapshot["revoked"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: Optional[List[str]] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: Optional[str] = None, - email_address: Optional[str] = None, - country_name: Optional[str] = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - certificate_revoked = EventSource(CertificateRevokedEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: Optional[str] = None, - certificate_signing_request: Optional[str] = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", [])) - for certificate in provider_certificates: - certificate["revoked"] = True - relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggered on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - if certificate.get("revoked", False): - self.on.certificate_revoked.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - revoked=True, - ) - else: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/openstack-exporter-k8s/osci.yaml b/charms/openstack-exporter-k8s/osci.yaml deleted file mode 100644 index b56ba0eb..00000000 --- a/charms/openstack-exporter-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: openstack-exporter-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/openstack-exporter-k8s/pyproject.toml b/charms/openstack-exporter-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/openstack-exporter-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/openstack-exporter-k8s/rename.sh b/charms/openstack-exporter-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/openstack-exporter-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/openstack-exporter-k8s/requirements.txt b/charms/openstack-exporter-k8s/requirements.txt index 3b37feec..48fb8600 100644 --- a/charms/openstack-exporter-k8s/requirements.txt +++ b/charms/openstack-exporter-k8s/requirements.txt @@ -1,5 +1,9 @@ ops jinja2 -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +lightkube +lightkube-models # COS requirement cosl + +# From ops_sunbeam +tenacity diff --git a/charms/openstack-exporter-k8s/test-requirements.txt b/charms/openstack-exporter-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/openstack-exporter-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py index ecc89af4..88c4f24e 100644 --- a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py +++ b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py @@ -18,6 +18,7 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( Mock, @@ -26,8 +27,6 @@ from ops.testing import ( Harness, ) -import charm - class _OSExporterTestOperatorCharm(charm.OSExporterOperatorCharm): """Test Operator Charm for Openstack Exporter Operator.""" diff --git a/charms/openstack-exporter-k8s/tox.ini b/charms/openstack-exporter-k8s/tox.ini deleted file mode 100644 index 0bc536c1..00000000 --- a/charms/openstack-exporter-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/openstack-hypervisor/.gitignore b/charms/openstack-hypervisor/.gitignore deleted file mode 100644 index 33d25ac9..00000000 --- a/charms/openstack-hypervisor/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -.stestr/ diff --git a/charms/openstack-hypervisor/.gitreview b/charms/openstack-hypervisor/.gitreview deleted file mode 100644 index afdea8f1..00000000 --- a/charms/openstack-hypervisor/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-openstack-hypervisor.git -defaultbranch=main diff --git a/charms/openstack-hypervisor/.stestr.conf b/charms/openstack-hypervisor/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/openstack-hypervisor/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/openstack-hypervisor/.zuul.yaml b/charms/openstack-hypervisor/.zuul.yaml deleted file mode 100644 index fd8a2c1d..00000000 --- a/charms/openstack-hypervisor/.zuul.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - prometheus-alert-rules-test - vars: - charm_build_name: openstack-hypervisor - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false - prometheus_alerts_test_rules_dir: tests/unit/test_alert_rules diff --git a/charms/openstack-hypervisor/charmcraft.yaml b/charms/openstack-hypervisor/charmcraft.yaml index c82f3030..9a8d8b29 100644 --- a/charms/openstack-hypervisor/charmcraft.yaml +++ b/charms/openstack-hypervisor/charmcraft.yaml @@ -23,4 +23,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py deleted file mode 100644 index 016e1ba2..00000000 --- a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py +++ /dev/null @@ -1,224 +0,0 @@ -"""CeilometerServiceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the ceilometer_service interface. - -Import `CeilometerServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "ceilometer_service" - -Two events are also available to respond to: - - config_changed - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.ceilometer_k8s.v0.ceilometer_service import ( - CeilometerServiceRequires -) - -class CeilometerServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CeilometerService Requires - self.ceilometer_service = CeilometerServiceRequires( - self, "ceilometer_service", - ) - self.framework.observe( - self.ceilometer_service.on.config_changed, - self._on_ceilometer_service_config_changed - ) - self.framework.observe( - self.ceilometer_service.on.goneaway, - self._on_ceiometer_service_goneaway - ) - - def _on_ceilometer_service_config_changed(self, event): - '''React to the Ceilometer service config changed event. - - This event happens when CeilometerService relation is added to the - model and relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - - def _on_ceilometer_service_goneaway(self, event): - '''React to the CeilometerService goneaway event. - - This event happens when CeilometerService relation is removed. - ''' - # CeilometerService Relation has goneaway. - pass -``` -""" - -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "fcbb94e7a18740729eaf9e2c3b90017f" - -# 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 - - -class CeilometerConfigRequestEvent(RelationEvent): - """CeilometerConfigRequest Event.""" - - pass - - -class CeilometerServiceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - config_request = EventSource(CeilometerConfigRequestEvent) - - -class CeilometerServiceProvides(Object): - """CeilometerServiceProvides class.""" - - on = CeilometerServiceProviderEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_ceilometer_service_relation_changed, - ) - - def _on_ceilometer_service_relation_changed( - self, event: RelationChangedEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService relation changed") - self.on.config_request.emit(event.relation) - - def set_config( - self, relation: Optional[Relation], telemetry_secret: str - ) -> None: - """Set ceilometer configuration on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping set config") - return - - # If relation is not provided send config to all the related - # applications. This happens usually when config data is - # updated by provider and wants to send the data to all - # related applications - if relation is None: - logging.debug( - "Sending config to all related applications of relation" - f"{self.relation_name}" - ) - for relation in self.framework.model.relations[self.relation_name]: - relation.data[self.charm.app][ - "telemetry-secret" - ] = telemetry_secret - else: - logging.debug( - f"Sending config on relation {relation.app.name} " - f"{relation.name}/{relation.id}" - ) - relation.data[self.charm.app][ - "telemetry-secret" - ] = telemetry_secret - - -class CeilometerConfigChangedEvent(RelationEvent): - """CeilometerConfigChanged Event.""" - - pass - - -class CeilometerServiceGoneAwayEvent(RelationEvent): - """CeilometerServiceGoneAway Event.""" - - pass - - -class CeilometerServiceRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - config_changed = EventSource(CeilometerConfigChangedEvent) - goneaway = EventSource(CeilometerServiceGoneAwayEvent) - - -class CeilometerServiceRequires(Object): - """CeilometerServiceRequires class.""" - - on = CeilometerServiceRequirerEvents() - - def __init__(self, charm: CharmBase, 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_changed, - self._on_ceilometer_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceilometer_service_relation_broken, - ) - - def _on_ceilometer_service_relation_changed( - self, event: RelationChangedEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService config data changed") - self.on.config_changed.emit(event.relation) - - def _on_ceilometer_service_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService on_broken") - self.on.goneaway.emit(event.relation) - - @property - def _ceilometer_service_rel(self) -> Optional[Relation]: - """The ceilometer service relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._ceilometer_service_rel: - data = self._ceilometer_service_rel.data[ - self._ceilometer_service_rel.app - ] - return data.get(key) - - return None - - @property - def telemetry_secret(self) -> Optional[str]: - """Return the telemetry_secret.""" - return self.get_remote_app_data("telemetry-secret") diff --git a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py deleted file mode 100644 index 94c5fe6d..00000000 --- a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py +++ /dev/null @@ -1,266 +0,0 @@ -"""CephAccess Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the ceph-access interface. - -Import `CephAccessRequires` in your charm, with the charm object and the -relation name: - - self - - "ceph_access" - -Three events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.cinder_ceph_k8s.v0.ceph_access import CephAccessRequires - -class CephAccessClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CephAccess Requires - self.ceph_access = CephAccessRequires( - self, - relation_name="ceph_access", - ) - self.framework.observe( - self.ceph_access.on.connected, self._on_ceph_access_connected) - self.framework.observe( - self.ceph_access.on.ready, self._on_ceph_access_ready) - self.framework.observe( - self.ceph_access.on.goneaway, self._on_ceph_access_goneaway) - - def _on_ceph_access_connected(self, event): - '''React to the CephAccess connected event. - - This event happens when n CephAccess relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_ceph_access_ready(self, event): - '''React to the CephAccess ready event. - - This event happens when an CephAccess relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass - -``` - -""" - -# The unique Charmhub library identifier, never change it -LIBID = "7fa8d4f8407c4f31ab1deb51c0c046f1" - -# 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 - -import logging -from ops.model import ( - Relation, - SecretNotFoundError, -) -from ops.framework import ( - EventBase, - ObjectEvents, - EventSource, - Object, -) -logger = logging.getLogger(__name__) - -class CephAccessConnectedEvent(EventBase): - """CephAccess connected Event.""" - - pass - - -class CephAccessReadyEvent(EventBase): - """CephAccess ready for use Event.""" - - pass - - -class CephAccessGoneAwayEvent(EventBase): - """CephAccess relation has gone-away Event""" - - pass - - -class CephAccessServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CephAccessConnectedEvent) - ready = EventSource(CephAccessReadyEvent) - goneaway = EventSource(CephAccessGoneAwayEvent) - - -class CephAccessRequires(Object): - """ - CephAccessRequires class - """ - - - on = CephAccessServerEvents() - - 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_ceph_access_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceph_access_relation_broken, - ) - - @property - def _ceph_access_rel(self) -> Relation: - """The CephAccess 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._ceph_access_rel.data[self._ceph_access_rel.app] - return data.get(key) - - def _on_ceph_access_relation_joined(self, event): - """CephAccess relation joined.""" - logging.debug("CephAccess on_joined") - self.on.connected.emit() - - def _on_ceph_access_relation_changed(self, event): - """CephAccess relation changed.""" - logging.debug("CephAccess on_changed") - try: - if self.ready: - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_ceph_access_relation_broken(self, event): - """CephAccess relation broken.""" - logging.debug("CephAccess on_broken") - self.on.goneaway.emit() - - def _retrieve_secret(self): - try: - credentials_id = self.get_remote_app_data('access-credentials') - if not credentials_id: - return None - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def ceph_access_data(self): - """Return the service_password.""" - secret = self._retrieve_secret() - if not secret: - return {} - return secret.get_content() - - @property - def ready(self) -> str: - """Return the service_password.""" - return all(k in self.ceph_access_data for k in ["uuid", "key"]) - -class HasCephAccessClientsEvent(EventBase): - """Has CephAccessClients Event.""" - - pass - -class ReadyCephAccessClientsEvent(EventBase): - """Has CephAccessClients Event.""" - - def __init__(self, handle, relation_id): - super().__init__(handle) - self.relation_id = relation_id - - def snapshot(self): - return {"relation_id": self.relation_id} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - -class CephAccessClientEvents(ObjectEvents): - """Events class for `on`""" - - has_ceph_access_clients = EventSource(HasCephAccessClientsEvent) - ready_ceph_access_clients = EventSource(ReadyCephAccessClientsEvent) - - -class CephAccessProvides(Object): - """ - CephAccessProvides class - """ - - on = CephAccessClientEvents() - - 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_ceph_access_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceph_access_relation_broken, - ) - - def _on_ceph_access_relation_joined(self, event): - """Handle CephAccess joined.""" - logging.debug("CephAccess on_joined") - self.on.has_ceph_access_clients.emit() - - def _on_ceph_access_relation_changed(self, event): - """Handle CephAccess joined.""" - logging.debug("CephAccess on_changed") - self.on.ready_ceph_access_clients.emit(event.relation.id) - - def _on_ceph_access_relation_broken(self, event): - """Handle CephAccess broken.""" - logging.debug("CephAccessProvides on_broken") - - def set_ceph_access_credentials(self, relation_name: int, - relation_id: str, - access_credentials: str): - - logging.debug("Setting ceph_access connection information.") - _ceph_access_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _ceph_access_rel = relation - if not _ceph_access_rel: - # Relation has disappeared so skip send of data - return - app_data = _ceph_access_rel.data[self.charm.app] - logging.debug(access_credentials) - app_data["access-credentials"] = access_credentials diff --git a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py b/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py deleted file mode 100644 index 6253f738..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py +++ /dev/null @@ -1,418 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_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.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_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.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "a5d96cc2686c47eea554ce2210c2d24e" - -# 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 = 2 - -logger = logging.getLogger(__name__) - - -class CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials 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._cloud_credentials_rel.data[self._cloud_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 username(self) -> str: - """Return the username.""" - return self.get_remote_app_data('username') - - @property - def password(self) -> str: - """Return the password.""" - return self.get_remote_app_data('password') - - @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 CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients 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 CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials 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_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_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, - username: str, - password: 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 cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_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["username"] = username - app_data["password"] = password - 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/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py deleted file mode 100644 index e3f4565d..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,458 +0,0 @@ -"""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 = 3 - -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') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - 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, - admin_role: 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 - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint - app_data["admin-role"] = admin_role diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py deleted file mode 100644 index e8d2773e..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py +++ /dev/null @@ -1,493 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import Relation - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 = 2 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_password(self) -> str: - """Return the service_password.""" - return self.get_remote_app_data('service-password') - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - return self.get_remote_app_data('service-user-name') - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_password: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-name"] = service_user.name - app_data["service-user-id"] = service_user.id - app_data["service-password"] = service_password - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py deleted file mode 100644 index 75ece456..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py +++ /dev/null @@ -1,451 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_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.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_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.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials 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 = "a5d96cc2686c47eea554ce2210c2d24e" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials 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._cloud_credentials_rel.data[self._cloud_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') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - def request_credentials(self) -> None: - """Request credentials from the CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients 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 CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _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_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials 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_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_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 cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_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 - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py deleted file mode 100644 index a3fb9109..00000000 --- a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# KubernetesServicePatch Library. - -This library is designed to enable developers to more simply patch the Kubernetes Service created -by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a -service named after the application in the namespace (named after the Juju model). This service by -default contains a "placeholder" port, which is 65536/TCP. - -When modifying the default set of resources managed by Juju, one must consider the lifecycle of the -charm. In this case, any modifications to the default service (created during deployment), will -be overwritten during a charm upgrade. - -When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` -events which applies the patch to the cluster. This should ensure that the service ports are -correct throughout the charm's life. - -The constructor simply takes a reference to the parent charm, and a list of tuples that each define -a port for the service, where each tuple contains: - -- a name for the port -- port for the service to listen on -- optionally: a targetPort for the service (the port in the container!) -- optionally: a nodePort for the service (for NodePort or LoadBalancer services only!) -- optionally: a name of the service (in case service name needs to be patched as well) - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. **Note -that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** - -```shell -cd some-charm -charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch -echo <<-EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For ClusterIP services: -```python -# ... -from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)]) - # ... -``` - -For LoadBalancer/NodePort services: -```python -# ... -from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.service_patcher = KubernetesServicePatch( - self, [(f"{self.app.name}", 443, 443, 30666)], "LoadBalancer" - ) - # ... -``` - -Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library -does not try to make any API calls, or open any files during testing that are unlikely to be -present, and could break your tests. The easiest way to do this is during your test `setUp`: - -```python -# ... - -@patch("charm.KubernetesServicePatch", lambda x, y: None) -def setUp(self, *unused): - self.harness = Harness(SomeCharm) - # ... -``` -""" - -import logging -from types import MethodType -from typing import Literal, Sequence, Tuple, Union - -from lightkube import ApiError, Client -from lightkube.models.core_v1 import ServicePort, ServiceSpec -from lightkube.models.meta_v1 import ObjectMeta -from lightkube.resources.core_v1 import Service -from lightkube.types import PatchType -from ops.charm import CharmBase -from ops.framework import Object - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0042f86d0a874435adef581806cddbbb" - -# 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 = 6 - -PortDefinition = Union[Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]] -ServiceType = Literal["ClusterIP", "LoadBalancer"] - - -class KubernetesServicePatch(Object): - """A utility for patching the Kubernetes service set up by Juju.""" - - def __init__( - self, - charm: CharmBase, - ports: Sequence[PortDefinition], - service_name: str = None, - service_type: ServiceType = "ClusterIP", - additional_labels: dict = None, - additional_selectors: dict = None, - additional_annotations: dict = None, - ): - """Constructor for KubernetesServicePatch. - - Args: - charm: the charm that is instantiating the library. - ports: a list of tuples (name, port, targetPort, nodePort) for every service port. - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - """ - super().__init__(charm, "kubernetes-service-patch") - self.charm = charm - self.service_name = service_name if service_name else self._app - self.service = self._service_object( - ports, - service_name, - service_type, - additional_labels, - additional_selectors, - additional_annotations, - ) - - # Make mypy type checking happy that self._patch is a method - assert isinstance(self._patch, MethodType) - # Ensure this patch is applied during the 'install' and 'upgrade-charm' events - self.framework.observe(charm.on.install, self._patch) - self.framework.observe(charm.on.upgrade_charm, self._patch) - - def _service_object( - self, - ports: Sequence[PortDefinition], - service_name: str = None, - service_type: ServiceType = "ClusterIP", - additional_labels: dict = None, - additional_selectors: dict = None, - additional_annotations: dict = None, - ) -> Service: - """Creates a valid Service representation. - - Args: - ports: a list of tuples of the form (name, port) or (name, port, targetPort) - or (name, port, targetPort, nodePort) for every service port. If the 'targetPort' - is omitted, it is assumed to be equal to 'port', with the exception of NodePort - and LoadBalancer services, where all port numbers have to be specified. - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - - Returns: - Service: A valid representation of a Kubernetes Service with the correct ports. - """ - if not service_name: - service_name = self._app - labels = {"app.kubernetes.io/name": self._app} - if additional_labels: - labels.update(additional_labels) - selector = {"app.kubernetes.io/name": self._app} - if additional_selectors: - selector.update(additional_selectors) - return Service( - apiVersion="v1", - kind="Service", - metadata=ObjectMeta( - namespace=self._namespace, - name=service_name, - labels=labels, - annotations=additional_annotations, # type: ignore[arg-type] - ), - spec=ServiceSpec( - selector=selector, - ports=[ - ServicePort( - name=p[0], - port=p[1], - targetPort=p[2] if len(p) > 2 else p[1], # type: ignore[misc] - nodePort=p[3] if len(p) > 3 else None, # type: ignore[arg-type, misc] - ) - for p in ports - ], - type=service_type, - ), - ) - - def _patch(self, _) -> None: - """Patch the Kubernetes service created by Juju to map the correct port. - - Raises: - PatchFailed: if patching fails due to lack of permissions, or otherwise. - """ - if not self.charm.unit.is_leader(): - return - - client = Client() - try: - if self.service_name != self._app: - self._delete_and_create_service(client) - client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) - except ApiError as e: - if e.status.code == 403: - logger.error("Kubernetes service patch failed: `juju trust` this application.") - else: - logger.error("Kubernetes service patch failed: %s", str(e)) - else: - logger.info("Kubernetes service '%s' patched successfully", self._app) - - def _delete_and_create_service(self, client: Client): - service = client.get(Service, self._app, namespace=self._namespace) - service.metadata.name = self.service_name # type: ignore[attr-defined] - service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 - client.delete(Service, self._app, namespace=self._namespace) - client.create(service) - - def is_patched(self) -> bool: - """Reports if the service patch has been applied. - - Returns: - bool: A boolean indicating if the service patch has been applied. - """ - client = Client() - # Get the relevant service from the cluster - service = client.get(Service, name=self.service_name, namespace=self._namespace) - # Construct a list of expected ports, should the patch be applied - expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] - # Construct a list in the same manner, using the fetched service - fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports] # type: ignore[attr-defined] # noqa: E501 - return expected_ports == fetched_ports - - @property - def _app(self) -> str: - """Name of the current Juju application. - - Returns: - str: A string containing the name of the current Juju application. - """ - return self.charm.app.name - - @property - def _namespace(self) -> str: - """The Kubernetes namespace we're running in. - - Returns: - str: A string containing the name of the current Kubernetes namespace. - """ - with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: - return f.read().strip() diff --git a/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 56cca01a..00000000 --- a/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# KubernetesServicePatch Library. - -This library is designed to enable developers to more simply patch the Kubernetes Service created -by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a -service named after the application in the namespace (named after the Juju model). This service by -default contains a "placeholder" port, which is 65536/TCP. - -When modifying the default set of resources managed by Juju, one must consider the lifecycle of the -charm. In this case, any modifications to the default service (created during deployment), will be -overwritten during a charm upgrade. - -When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` -events which applies the patch to the cluster. This should ensure that the service ports are -correct throughout the charm's life. - -The constructor simply takes a reference to the parent charm, and a list of -[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the -service. For information regarding the `lightkube` `ServicePort` model, please visit the -`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport). - -Optionally, a name of the service (in case service name needs to be patched as well), labels, -selectors, and annotations can be provided as keyword arguments. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. **Note -that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** - -```shell -cd some-charm -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch(self, [port]) - # ... -``` - -For `LoadBalancer`/`NodePort` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666) - self.service_patcher = KubernetesServicePatch( - self, [port], "LoadBalancer" - ) - # ... -``` - -Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"` - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP") - udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP") - sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP") - self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library -does not try to make any API calls, or open any files during testing that are unlikely to be -present, and could break your tests. The easiest way to do this is during your test `setUp`: - -```python -# ... - -@patch("charm.KubernetesServicePatch", lambda x, y: None) -def setUp(self, *unused): - self.harness = Harness(SomeCharm) - # ... -``` -""" - -import logging -from types import MethodType -from typing import List, Literal, Optional, Union - -from lightkube import ApiError, Client -from lightkube.core import exceptions -from lightkube.models.core_v1 import ServicePort, ServiceSpec -from lightkube.models.meta_v1 import ObjectMeta -from lightkube.resources.core_v1 import Service -from lightkube.types import PatchType -from ops.charm import CharmBase -from ops.framework import BoundEvent, Object - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0042f86d0a874435adef581806cddbbb" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -ServiceType = Literal["ClusterIP", "LoadBalancer"] - - -class KubernetesServicePatch(Object): - """A utility for patching the Kubernetes service set up by Juju.""" - - def __init__( - self, - charm: CharmBase, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, - ): - """Constructor for KubernetesServicePatch. - - Args: - charm: the charm that is instantiating the library. - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - super().__init__(charm, "kubernetes-service-patch") - self.charm = charm - self.service_name = service_name if service_name else self._app - self.service = self._service_object( - ports, - service_name, - service_type, - additional_labels, - additional_selectors, - additional_annotations, - ) - - # Make mypy type checking happy that self._patch is a method - assert isinstance(self._patch, MethodType) - # Ensure this patch is applied during the 'install' and 'upgrade-charm' events - self.framework.observe(charm.on.install, self._patch) - self.framework.observe(charm.on.upgrade_charm, self._patch) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - ) -> Service: - """Creates a valid Service representation. - - Args: - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - - Returns: - Service: A valid representation of a Kubernetes Service with the correct ports. - """ - if not service_name: - service_name = self._app - labels = {"app.kubernetes.io/name": self._app} - if additional_labels: - labels.update(additional_labels) - selector = {"app.kubernetes.io/name": self._app} - if additional_selectors: - selector.update(additional_selectors) - return Service( - apiVersion="v1", - kind="Service", - metadata=ObjectMeta( - namespace=self._namespace, - name=service_name, - labels=labels, - annotations=additional_annotations, # type: ignore[arg-type] - ), - spec=ServiceSpec( - selector=selector, - ports=ports, - type=service_type, - ), - ) - - def _patch(self, _) -> None: - """Patch the Kubernetes service created by Juju to map the correct port. - - Raises: - PatchFailed: if patching fails due to lack of permissions, or otherwise. - """ - try: - client = Client() - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - if self.service_name != self._app: - self._delete_and_create_service(client) - client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) - except ApiError as e: - if e.status.code == 403: - logger.error("Kubernetes service patch failed: `juju trust` this application.") - else: - logger.error("Kubernetes service patch failed: %s", str(e)) - else: - logger.info("Kubernetes service '%s' patched successfully", self._app) - - def _delete_and_create_service(self, client: Client): - service = client.get(Service, self._app, namespace=self._namespace) - service.metadata.name = self.service_name # type: ignore[attr-defined] - service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 - client.delete(Service, self._app, namespace=self._namespace) - client.create(service) - - def is_patched(self) -> bool: - """Reports if the service patch has been applied. - - Returns: - bool: A boolean indicating if the service patch has been applied. - """ - client = Client() - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - else: - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # Construct a list of expected ports, should the patch be applied - expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] - # Construct a list in the same manner, using the fetched service - fetched_ports = [ - (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] - ] # noqa: E501 - return expected_ports == fetched_ports - - @property - def _app(self) -> str: - """Name of the current Juju application. - - Returns: - str: A string containing the name of the current Juju application. - """ - return self.charm.app.name - - @property - def _namespace(self) -> str: - """The Kubernetes namespace we're running in. - - Returns: - str: A string containing the name of the current Kubernetes namespace. - """ - with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: - return f.read().strip() diff --git a/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index ef016e23..00000000 --- a/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,218 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# 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 = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def public_address(self): - relation = self.framework.model.get_relation(self.relation_name) - data = relation.data[relation.app] - return data.get('public-address') - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v - - def set_app_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the app data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.charm.app][k] = v diff --git a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py b/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py deleted file mode 100644 index e1769e8c..00000000 --- a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py +++ /dev/null @@ -1,558 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v1.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" - -import logging -import socket -import typing -from typing import Any, Dict, Optional, Tuple, Union - -import yaml -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 5 - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) - -try: - import jsonschema - - DO_VALIDATION = True -except ModuleNotFoundError: - log.warning( - "The `ingress` library needs the `jsonschema` package to be able " - "to do runtime data validation; without it, it will still work but validation " - "will be disabled. \n" - "It is recommended to add `jsonschema` to the 'requirements.txt' of your charm, " - "which will enable this feature." - ) - DO_VALIDATION = False - -INGRESS_REQUIRES_APP_SCHEMA = { - "type": "object", - "properties": { - "model": {"type": "string"}, - "name": {"type": "string"}, - "host": {"type": "string"}, - "port": {"type": "string"}, - "strip-prefix": {"type": "string"}, - }, - "required": ["model", "name", "host", "port"], -} - -INGRESS_PROVIDES_APP_SCHEMA = { - "type": "object", - "properties": { - "ingress": {"type": "object", "properties": {"url": {"type": "string"}}}, - }, - "required": ["ingress"], -} - -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict # py35 compat - -# Model of the data a unit implementing the requirer will need to provide. -RequirerData = TypedDict( - "RequirerData", - {"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool}, - total=False, -) -# Provider ingress data model. -ProviderIngressData = TypedDict("ProviderIngressData", {"url": str}) -# Provider application databag model. -ProviderApplicationData = TypedDict("ProviderApplicationData", {"ingress": ProviderIngressData}) - - -def _validate_data(data, schema): - """Checks whether `data` matches `schema`. - - Will raise DataValidationError if the data is not valid, else return None. - """ - if not DO_VALIDATION: - return - try: - jsonschema.validate(instance=data, schema=schema) - except jsonschema.ValidationError as e: - raise DataValidationError(data, schema) from e - - -class DataValidationError(RuntimeError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__ = () # type: Tuple[str, ...] - __optional_kwargs__ = {} # type: Dict[str, Any] - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self) -> dict: - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot: dict) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "port", "host", "strip_prefix") - - if typing.TYPE_CHECKING: - name = None # type: str - model = None # type: str - port = None # type: int - host = None # type: str - strip_prefix = False # type: bool - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self._get_requirer_data(event.relation) - self.on.data_provided.emit( - event.relation, - data["name"], - data["model"], - data["port"], - data["host"], - data.get("strip-prefix", False), - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_data(self, relation: Relation) -> RequirerData: - """Fetch and validate the requirer's app databag. - - For convenience, we convert 'port' to integer. - """ - if not all((relation.app, relation.app.name)): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return {} - - databag = relation.data[relation.app] - remote_data = {} # type: Dict[str, Union[int, str]] - for k in ("port", "host", "model", "name", "mode", "strip-prefix"): - v = databag.get(k) - if v is not None: - remote_data[k] = v - _validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA) - remote_data["port"] = int(remote_data["port"]) - remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False)) - return remote_data - - def get_data(self, relation: Relation) -> RequirerData: - """Fetch the remote app's databag, i.e. the requirer data.""" - return self._get_requirer_data(relation) - - def is_ready(self, relation: Relation = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - return bool(self._get_requirer_data(relation)) - except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _provided_url(self, relation: Relation) -> ProviderIngressData: - """Fetch and validate this app databag; return the ingress url.""" - if not all((relation.app, relation.app.name, self.unit.is_leader())): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return {} # noqa - - # fetch the provider's app databag - raw_data = relation.data[self.app].get("ingress") - if not raw_data: - raise RuntimeError("This application did not `publish_url` yet.") - - ingress: ProviderIngressData = yaml.safe_load(raw_data) - _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA) - return ingress - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress = {"url": url} - ingress_data = {"ingress": ingress} - _validate_data(ingress_data, INGRESS_PROVIDES_APP_SCHEMA) - relation.data[self.app]["ingress"] = yaml.safe_dump(ingress) - - @property - def proxied_endpoints(self): - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - results[ingress_relation.app.name] = self._provided_url(ingress_relation) - - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url = None # type: str - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() - # used to prevent spur1ious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: str = None, - port: int = None, - strip_prefix: bool = False, - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - - self._stored.set_default(current_url=None) - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data(event.relation) - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: - self._stored.current_url = new_url - self.on.ready.emit(event.relation, new_url) - - def _handle_relation_broken(self, event): - self._stored.current_url = None - self.on.revoked.emit(event.relation) - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - for relation in self.relations: - self._publish_auto_data(relation) - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self, relation: Relation): - if self._auto_data and self.unit.is_leader(): - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements(self, *, host: str = None, port: int): - """Publishes the data that Traefik needs to provide ingress. - - NB only the leader unit is supposed to do this. - - Args: - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - # get only the leader to publish the data since we only - # require one unit to publish it -- it will not differ between units, - # unlike in ingress-per-unit. - assert self.unit.is_leader(), "only leaders should do this." - assert self.relation, "no relation" - - if not host: - host = socket.getfqdn() - - data = { - "model": self.model.name, - "name": self.app.name, - "host": host, - "port": str(port), - } - - if self._strip_prefix: - data["strip-prefix"] = "true" - - _validate_data(data, INGRESS_REQUIRES_APP_SCHEMA) - self.relation.data[self.app].update(data) - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation: - return None - - # fetch the provider's app databag - try: - raw = relation.data.get(relation.app, {}).get("ingress") - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not raw: - return None - - ingress: ProviderIngressData = yaml.safe_load(raw) - _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA) - return ingress["url"] - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = self._stored.current_url or None # type: ignore - assert isinstance(data, (str, type(None))) # for static checker - return data diff --git a/charms/openstack-hypervisor/osci.yaml b/charms/openstack-hypervisor/osci.yaml deleted file mode 100644 index 91848efd..00000000 --- a/charms/openstack-hypervisor/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: openstack-hypervisor - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/openstack-hypervisor/pyproject.toml b/charms/openstack-hypervisor/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/openstack-hypervisor/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/openstack-hypervisor/rename.sh b/charms/openstack-hypervisor/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/openstack-hypervisor/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/openstack-hypervisor/requirements.txt b/charms/openstack-hypervisor/requirements.txt index 17f89a30..978f759e 100644 --- a/charms/openstack-hypervisor/requirements.txt +++ b/charms/openstack-hypervisor/requirements.txt @@ -7,8 +7,9 @@ jinja2 cosl==0.0.5 ; python_version >= "3.8" pydantic==1.10.12 ; python_version >= "3.8" -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - # This charm does not use lightkube* but ops_sunbeam requires it atm lightkube lightkube-models + +# From ops_sunbeam +tenacity diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index d77370a7..d8ad6b9f 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -51,7 +51,6 @@ from ops.charm import ( from ops.main import ( main, ) - from utils import ( get_local_ip_by_default_route, ) diff --git a/charms/openstack-hypervisor/test-requirements.txt b/charms/openstack-hypervisor/test-requirements.txt deleted file mode 100644 index 0982ac98..00000000 --- a/charms/openstack-hypervisor/test-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -ops -cosl==0.0.5 ; python_version >= "3.8" -pydantic==1.10.12 ; python_version >= "3.8" -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index 9b89b138..dbaba261 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -20,9 +20,8 @@ from unittest import ( mock, ) -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _HypervisorOperatorCharm(charm.HypervisorOperatorCharm): diff --git a/charms/openstack-hypervisor/tox.ini b/charms/openstack-hypervisor/tox.ini deleted file mode 100644 index 38807328..00000000 --- a/charms/openstack-hypervisor/tox.ini +++ /dev/null @@ -1,168 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ovn-central-k8s/.flake8 b/charms/ovn-central-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/ovn-central-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/ovn-central-k8s/.gitignore b/charms/ovn-central-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/ovn-central-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/ovn-central-k8s/.gitreview b/charms/ovn-central-k8s/.gitreview deleted file mode 100644 index 95d816b4..00000000 --- a/charms/ovn-central-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=x/charm-ovn-central-k8s.git -defaultbranch=main diff --git a/charms/ovn-central-k8s/.jujuignore b/charms/ovn-central-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/ovn-central-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/ovn-central-k8s/.stestr.conf b/charms/ovn-central-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ovn-central-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ovn-central-k8s/.zuul.yaml b/charms/ovn-central-k8s/.zuul.yaml deleted file mode 100644 index 0a41ba84..00000000 --- a/charms/ovn-central-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ovn-central-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ovn-central-k8s/charmcraft.yaml b/charms/ovn-central-k8s/charmcraft.yaml index eea1df04..e67d37b5 100644 --- a/charms/ovn-central-k8s/charmcraft.yaml +++ b/charms/ovn-central-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ovn-central-k8s/fetch-libs.sh b/charms/ovn-central-k8s/fetch-libs.sh deleted file mode 100755 index 2b40c7b4..00000000 --- a/charms/ovn-central-k8s/fetch-libs.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates diff --git a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# 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 = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/ovn-central-k8s/osci.yaml b/charms/ovn-central-k8s/osci.yaml deleted file mode 100644 index 15e9a479..00000000 --- a/charms/ovn-central-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ovn-central-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 23.09/edge diff --git a/charms/ovn-central-k8s/pyproject.toml b/charms/ovn-central-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/ovn-central-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ovn-central-k8s/rename.sh b/charms/ovn-central-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ovn-central-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ovn-central-k8s/requirements.txt b/charms/ovn-central-k8s/requirements.txt index 9211d667..ddbedec1 100644 --- a/charms/ovn-central-k8s/requirements.txt +++ b/charms/ovn-central-k8s/requirements.txt @@ -11,4 +11,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/ovn-central-k8s/src/charm.py b/charms/ovn-central-k8s/src/charm.py index ad0971ee..7158c4a7 100755 --- a/charms/ovn-central-k8s/src/charm.py +++ b/charms/ovn-central-k8s/src/charm.py @@ -35,6 +35,8 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts import ops_sunbeam.ovn.container_handlers as ovn_chandlers import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ovn +import ovsdb as ch_ovsdb import tenacity from ops.framework import ( StoredState, @@ -43,9 +45,6 @@ from ops.main import ( main, ) -import ovn -import ovsdb as ch_ovsdb - logger = logging.getLogger(__name__) OVN_SB_DB_CONTAINER = "ovn-sb-db-server" diff --git a/charms/ovn-central-k8s/test-requirements.txt b/charms/ovn-central-k8s/test-requirements.txt deleted file mode 100644 index da3b04b7..00000000 --- a/charms/ovn-central-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py index 2b4e5c08..ab676826 100644 --- a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py +++ b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py @@ -16,11 +16,10 @@ """Tests for OVN central charm.""" +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _OVNCentralOperatorCharm(charm.OVNCentralOperatorCharm): def __init__(self, framework): diff --git a/charms/ovn-central-k8s/tox.ini b/charms/ovn-central-k8s/tox.ini deleted file mode 100644 index 2b8f98b1..00000000 --- a/charms/ovn-central-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ovn-relay-k8s/.flake8 b/charms/ovn-relay-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/ovn-relay-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/ovn-relay-k8s/.gitignore b/charms/ovn-relay-k8s/.gitignore deleted file mode 100644 index de9170b0..00000000 --- a/charms/ovn-relay-k8s/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ diff --git a/charms/ovn-relay-k8s/.gitreview b/charms/ovn-relay-k8s/.gitreview deleted file mode 100644 index 5f989225..00000000 --- a/charms/ovn-relay-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=x/charm-ovn-relay-k8s.git -defaultbranch=main diff --git a/charms/ovn-relay-k8s/.jujuignore b/charms/ovn-relay-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/ovn-relay-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/ovn-relay-k8s/.stestr.conf b/charms/ovn-relay-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ovn-relay-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ovn-relay-k8s/.zuul.yaml b/charms/ovn-relay-k8s/.zuul.yaml deleted file mode 100644 index 494e48c6..00000000 --- a/charms/ovn-relay-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-yoga-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ovn-relay-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ovn-relay-k8s/charmcraft.yaml b/charms/ovn-relay-k8s/charmcraft.yaml index eea1df04..e67d37b5 100644 --- a/charms/ovn-relay-k8s/charmcraft.yaml +++ b/charms/ovn-relay-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ovn-relay-k8s/fetch-libs.sh b/charms/ovn-relay-k8s/fetch-libs.sh deleted file mode 100755 index bbf34c7b..00000000 --- a/charms/ovn-relay-k8s/fetch-libs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates diff --git a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 56cca01a..00000000 --- a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# KubernetesServicePatch Library. - -This library is designed to enable developers to more simply patch the Kubernetes Service created -by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a -service named after the application in the namespace (named after the Juju model). This service by -default contains a "placeholder" port, which is 65536/TCP. - -When modifying the default set of resources managed by Juju, one must consider the lifecycle of the -charm. In this case, any modifications to the default service (created during deployment), will be -overwritten during a charm upgrade. - -When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` -events which applies the patch to the cluster. This should ensure that the service ports are -correct throughout the charm's life. - -The constructor simply takes a reference to the parent charm, and a list of -[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the -service. For information regarding the `lightkube` `ServicePort` model, please visit the -`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport). - -Optionally, a name of the service (in case service name needs to be patched as well), labels, -selectors, and annotations can be provided as keyword arguments. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. **Note -that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** - -```shell -cd some-charm -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch(self, [port]) - # ... -``` - -For `LoadBalancer`/`NodePort` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666) - self.service_patcher = KubernetesServicePatch( - self, [port], "LoadBalancer" - ) - # ... -``` - -Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"` - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP") - udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP") - sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP") - self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library -does not try to make any API calls, or open any files during testing that are unlikely to be -present, and could break your tests. The easiest way to do this is during your test `setUp`: - -```python -# ... - -@patch("charm.KubernetesServicePatch", lambda x, y: None) -def setUp(self, *unused): - self.harness = Harness(SomeCharm) - # ... -``` -""" - -import logging -from types import MethodType -from typing import List, Literal, Optional, Union - -from lightkube import ApiError, Client -from lightkube.core import exceptions -from lightkube.models.core_v1 import ServicePort, ServiceSpec -from lightkube.models.meta_v1 import ObjectMeta -from lightkube.resources.core_v1 import Service -from lightkube.types import PatchType -from ops.charm import CharmBase -from ops.framework import BoundEvent, Object - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0042f86d0a874435adef581806cddbbb" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -ServiceType = Literal["ClusterIP", "LoadBalancer"] - - -class KubernetesServicePatch(Object): - """A utility for patching the Kubernetes service set up by Juju.""" - - def __init__( - self, - charm: CharmBase, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, - ): - """Constructor for KubernetesServicePatch. - - Args: - charm: the charm that is instantiating the library. - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - super().__init__(charm, "kubernetes-service-patch") - self.charm = charm - self.service_name = service_name if service_name else self._app - self.service = self._service_object( - ports, - service_name, - service_type, - additional_labels, - additional_selectors, - additional_annotations, - ) - - # Make mypy type checking happy that self._patch is a method - assert isinstance(self._patch, MethodType) - # Ensure this patch is applied during the 'install' and 'upgrade-charm' events - self.framework.observe(charm.on.install, self._patch) - self.framework.observe(charm.on.upgrade_charm, self._patch) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - ) -> Service: - """Creates a valid Service representation. - - Args: - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - - Returns: - Service: A valid representation of a Kubernetes Service with the correct ports. - """ - if not service_name: - service_name = self._app - labels = {"app.kubernetes.io/name": self._app} - if additional_labels: - labels.update(additional_labels) - selector = {"app.kubernetes.io/name": self._app} - if additional_selectors: - selector.update(additional_selectors) - return Service( - apiVersion="v1", - kind="Service", - metadata=ObjectMeta( - namespace=self._namespace, - name=service_name, - labels=labels, - annotations=additional_annotations, # type: ignore[arg-type] - ), - spec=ServiceSpec( - selector=selector, - ports=ports, - type=service_type, - ), - ) - - def _patch(self, _) -> None: - """Patch the Kubernetes service created by Juju to map the correct port. - - Raises: - PatchFailed: if patching fails due to lack of permissions, or otherwise. - """ - try: - client = Client() - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - if self.service_name != self._app: - self._delete_and_create_service(client) - client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) - except ApiError as e: - if e.status.code == 403: - logger.error("Kubernetes service patch failed: `juju trust` this application.") - else: - logger.error("Kubernetes service patch failed: %s", str(e)) - else: - logger.info("Kubernetes service '%s' patched successfully", self._app) - - def _delete_and_create_service(self, client: Client): - service = client.get(Service, self._app, namespace=self._namespace) - service.metadata.name = self.service_name # type: ignore[attr-defined] - service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 - client.delete(Service, self._app, namespace=self._namespace) - client.create(service) - - def is_patched(self) -> bool: - """Reports if the service patch has been applied. - - Returns: - bool: A boolean indicating if the service patch has been applied. - """ - client = Client() - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - else: - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # Construct a list of expected ports, should the patch be applied - expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] - # Construct a list in the same manner, using the fetched service - fetched_ports = [ - (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] - ] # noqa: E501 - return expected_ports == fetched_ports - - @property - def _app(self) -> str: - """Name of the current Juju application. - - Returns: - str: A string containing the name of the current Juju application. - """ - return self.charm.app.name - - @property - def _namespace(self) -> str: - """The Kubernetes namespace we're running in. - - Returns: - str: A string containing the name of the current Kubernetes namespace. - """ - with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: - return f.read().strip() diff --git a/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# 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 = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _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_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/ovn-relay-k8s/osci.yaml b/charms/ovn-relay-k8s/osci.yaml deleted file mode 100644 index 352efb71..00000000 --- a/charms/ovn-relay-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ovn-relay-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 23.09/edge diff --git a/charms/ovn-relay-k8s/pyproject.toml b/charms/ovn-relay-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/ovn-relay-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ovn-relay-k8s/rename.sh b/charms/ovn-relay-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ovn-relay-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ovn-relay-k8s/requirements.txt b/charms/ovn-relay-k8s/requirements.txt index 9211d667..ddbedec1 100644 --- a/charms/ovn-relay-k8s/requirements.txt +++ b/charms/ovn-relay-k8s/requirements.txt @@ -11,4 +11,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/ovn-relay-k8s/test-requirements.txt b/charms/ovn-relay-k8s/test-requirements.txt deleted file mode 100644 index da3b04b7..00000000 --- a/charms/ovn-relay-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py index f4654d66..475e4d55 100644 --- a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py +++ b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py @@ -16,9 +16,8 @@ """Tests for OVN relay.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _OVNRelayOperatorCharm(charm.OVNRelayOperatorCharm): diff --git a/charms/ovn-relay-k8s/tox.ini b/charms/ovn-relay-k8s/tox.ini deleted file mode 100644 index b0d1dd7b..00000000 --- a/charms/ovn-relay-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/placement-k8s/.flake8 b/charms/placement-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/placement-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/placement-k8s/.gitignore b/charms/placement-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/placement-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/placement-k8s/.gitreview b/charms/placement-k8s/.gitreview deleted file mode 100644 index 5d944736..00000000 --- a/charms/placement-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-placement-k8s.git -defaultbranch=main diff --git a/charms/placement-k8s/.jujuignore b/charms/placement-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/placement-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/placement-k8s/.stestr.conf b/charms/placement-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/placement-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/placement-k8s/.zuul.yaml b/charms/placement-k8s/.zuul.yaml deleted file mode 100644 index 5f56197e..00000000 --- a/charms/placement-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: placement-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/placement-k8s/charmcraft.yaml b/charms/placement-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/placement-k8s/charmcraft.yaml +++ b/charms/placement-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/placement-k8s/fetch-libs.sh b/charms/placement-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/placement-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -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.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - 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_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-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 service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-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 service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/placement-k8s/osci.yaml b/charms/placement-k8s/osci.yaml deleted file mode 100644 index 60172b1c..00000000 --- a/charms/placement-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: placement-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/placement-k8s/pyproject.toml b/charms/placement-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/placement-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/placement-k8s/rename.sh b/charms/placement-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/placement-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/placement-k8s/requirements.txt b/charms/placement-k8s/requirements.txt index e988c61d..64399ba9 100644 --- a/charms/placement-k8s/requirements.txt +++ b/charms/placement-k8s/requirements.txt @@ -12,4 +12,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/placement-k8s/src/templates/parts/section-database b/charms/placement-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/placement-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/placement-k8s/src/templates/parts/section-federation b/charms/placement-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/placement-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/placement-k8s/src/templates/parts/section-identity b/charms/placement-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/placement-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/placement-k8s/src/templates/parts/section-middleware b/charms/placement-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/placement-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/placement-k8s/src/templates/parts/section-service-user b/charms/placement-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/placement-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/placement-k8s/src/templates/parts/section-signing b/charms/placement-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/placement-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/placement-k8s/src/templates/placement.conf b/charms/placement-k8s/src/templates/placement.conf index c8f3673c..771165cf 100644 --- a/charms/placement-k8s/src/templates/placement.conf +++ b/charms/placement-k8s/src/templates/placement.conf @@ -5,11 +5,7 @@ debug = {{ options.debug }} auth_strategy = keystone [placement_database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/placement/placement.db -{% endif -%} +{% include "parts/database-connection" %} {% include "parts/section-identity" %} diff --git a/charms/placement-k8s/test-requirements.txt b/charms/placement-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/placement-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/placement-k8s/tests/unit/test_placement_charm.py b/charms/placement-k8s/tests/unit/test_placement_charm.py index 21ed982f..dc5da6b7 100644 --- a/charms/placement-k8s/tests/unit/test_placement_charm.py +++ b/charms/placement-k8s/tests/unit/test_placement_charm.py @@ -18,9 +18,8 @@ import textwrap -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _PlacementOperatorCharm(charm.PlacementOperatorCharm): @@ -115,6 +114,8 @@ class TestPlacementOperatorCharm(test_utils.CharmTestCase): [placement_database] connection = mysql+pymysql://foo:hardpassword@10.0.0.10/placement_api + + [keystone_authtoken] auth_url = http://keystone.internal:5000 interface = internal diff --git a/charms/placement-k8s/tox.ini b/charms/placement-k8s/tox.ini deleted file mode 100644 index 0ab1edc3..00000000 --- a/charms/placement-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - lib/* - -[flake8] -ignore=E226,W504 diff --git a/common.sh b/common.sh new file mode 100644 index 00000000..eb5e5d76 --- /dev/null +++ b/common.sh @@ -0,0 +1,418 @@ +#!/bin/bash + +# All libraries required by sunbeam charms are centrally +# maintained in libs folder. The libraries created by +# sunbeam charms are placed in libs/internal and the +# libraries provided by external charms are maintained +# in libs/external. +# All generic template parts are maintained in +# templates/parts folder. +# +# This script provides functions for each sunbeam charms +# all the common files that should be copied to charm +# for building the charm and function testing. + + +NULL_ARRAY=() + +# Internal libs for component. If libs are repeated, reuse the existing component +INTERNAL_CEILOMETER_LIBS=( + "keystone_k8s" + "ceilometer_k8s" + "gnocchi_k8s" +) + +INTERNAL_CINDER_LIBS=( + "keystone_k8s" + "cinder_k8s" +) + +INTERNAL_CINDER_CEPH_LIBS=( + "keystone_k8s" + "cinder_k8s" + "cinder_ceph_k8s" +) + +INTERNAL_DESIGNATE_LIBS=( + "keystone_k8s" + "designate_bind_k8s" +) + +INTERNAL_DESIGNATE_BIND_LIBS=( + "designate_bind_k8s" +) + +INTERNAL_GNOCCHI_LIBS=( + "keystone_k8s" + "gnocchi_k8s" +) + +INTERNAL_KEYSTONE_LIBS=( + "keystone_k8s" +) + +INTERNAL_NEUTRON_LIBS=( + "keystone_k8s" + "ovn_central_k8s" +) + +INTERNAL_NOVA_LIBS=( + "keystone_k8s" + "sunbeam_nova_compute_operator" +) + +INTERNAL_OPENSTACK_HYPERVISOR_LIBS=( + "keystone_k8s" + "ovn_central_k8s" + "cinder_ceph_k8s" + "ceilometer_k8s" +) + +INTERNAL_OVN_CENTRAL_LIBS=( + "ovn_central_k8s" +) + +# External libs for component. If libs are repeated, reuse the existing component +EXTERNAL_AODH_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" +) + +EXTERNAL_BARBICAN_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" + "vault_k8s" +) + +EXTERNAL_CEILOMETER_LIBS=( + "rabbitmq_k8s" +) + +EXTERNAL_DESIGNATE_BIND_LIBS=( + "observability_libs" +) + +EXTERNAL_HEAT_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_route_k8s" +) + +EXTERNAL_NEUTRON_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OCTAVIA_LIBS=( + "data_platform_libs" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OPENSTACK_EXPORTER_LIBS=( + "grafana_k8s" + "prometheus_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OPENSTACK_HYPERVISOR_LIBS=( + "data_platform_libs" + "grafana_agent" + "observability_libs" + "operator_libs_linux" + "rabbitmq_k8s" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OVN_CENTRAL_LIBS=( + "tls_certificates_interface" +) + +EXTERNAL_OVN_RELAY_LIBS=( + "tls_certificates_interface" + "observability_libs" +) + +# Config template parts for each component. +CONFIG_TEMPLATES_AODH=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-credentials" +) + +CONFIG_TEMPLATES_BARBICAN=( + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_CEILOMETER=( + "identity-data-id-creds" + "section-oslo-messaging-rabbit" + "section-service-credentials-from-identity-service" + "section-service-user-from-identity-credentials" +) + +CONFIG_TEMPLATES_CINDER=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_CINDER_CEPH=( + "section-oslo-messaging-rabbit" + "section-oslo-notifications" +) + +CONFIG_TEMPLATES_DESIGNATE=( + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_GLANCE=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-oslo-notifications" + "section-service-user" +) + +CONFIG_TEMPLATES_GNOCCHI=( + "database-connection" + "section-identity" + "identity-data" +) + +CONFIG_TEMPLATES_HEAT=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" +) + +CONFIG_TEMPLATES_KEYSTONE=( + "section-database" + "database-connection" + "section-federation" + "section-middleware" + "section-oslo-cache" + "section-oslo-messaging-rabbit" + "section-oslo-middleware" + "section-oslo-notifications" + "section-signing" +) + +CONFIG_TEMPLATES_MAGNUM=( + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" + "section-trust" +) + +CONFIG_TEMPLATES_NEUTRON=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_NOVA=${CONFIG_TEMPLATES_NEUTRON[@]} + +CONFIG_TEMPLATES_OCTAVIA=( + "section-database" + "database-connection" + "section-identity" + "identity-data" +) + +CONFIG_TEMPLATES_PLACEMENT=( + "database-connection" + "section-identity" + "identity-data" + "section-service-user" +) + +declare -A INTERNAL_LIBS=( + [aodh-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [barbican-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [ceilometer-k8s]=${INTERNAL_CEILOMETER_LIBS[@]} + [cinder-k8s]=${INTERNAL_CINDER_LIBS[@]} + [cinder-ceph-k8s]=${INTERNAL_CINDER_CEPH_LIBS[@]} + [designate-k8s]=${INTERNAL_DESIGNATE_LIBS[@]} + [designate-bind-k8s]=${INTERNAL_DESIGNATE_BIND_LIBS[@]} + [glance-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [gnocchi-k8s]=${INTERNAL_GNOCCHI_LIBS[@]} + [heat-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [horizon-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [keystone-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [keystone-ldap-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [magnum-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [neutron-k8s]=${INTERNAL_NEUTRON_LIBS[@]} + [nova-k8s]=${INTERNAL_NOVA_LIBS[@]} + [octavia-k8s]=${INTERNAL_NEUTRON_LIBS[@]} + [openstack-exporter-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [openstack-hypervisor]=${INTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]} + [ovn-central-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]} + [ovn-relay-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]} + [placement-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} +) + +declare -A EXTERNAL_LIBS=( + [aodh-k8s]=${EXTERNAL_AODH_LIBS[@]} + [barbican-k8s]=${EXTERNAL_BARBICAN_LIBS[@]} + [ceilometer-k8s]=${EXTERNAL_CEILOMETER_LIBS[@]} + [cinder-k8s]=${EXTERNAL_AODH_LIBS[@]} + [cinder-ceph-k8s]=${EXTERNAL_AODH_LIBS[@]} + [designate-k8s]=${EXTERNAL_AODH_LIBS[@]} + [designate-bind-k8s]=${EXTERNAL_DESIGNATE_BIND_LIBS[@]} + [glance-k8s]=${EXTERNAL_AODH_LIBS[@]} + [gnocchi-k8s]=${EXTERNAL_AODH_LIBS[@]} + [heat-k8s]=${EXTERNAL_HEAT_LIBS[@]} + [horizon-k8s]=${EXTERNAL_AODH_LIBS[@]} + [keystone-k8s]=${EXTERNAL_AODH_LIBS[@]} + [keystone-ldap-k8s]=${NULL_ARRAY[@]} + [magnum-k8s]=${EXTERNAL_AODH_LIBS[@]} + [neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]} + [nova-k8s]=${EXTERNAL_AODH_LIBS[@]} + [octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]} + [openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]} + [openstack-hypervisor]=${EXTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]} + [ovn-central-k8s]=${EXTERNAL_OVN_CENTRAL_LIBS[@]} + [ovn-relay-k8s]=${EXTERNAL_OVN_RELAY_LIBS[@]} + [placement-k8s]=${EXTERNAL_AODH_LIBS[@]} +) + +declare -A CONFIG_TEMPLATES=( + [aodh-k8s]=${CONFIG_TEMPLATES_AODH[@]} + [barbican-k8s]=${CONFIG_TEMPLATES_BARBICAN[@]} + [ceilometer-k8s]=${CONFIG_TEMPLATES_CEILOMETER[@]} + [cinder-k8s]=${CONFIG_TEMPLATES_CINDER[@]} + [cinder-ceph-k8s]=${CONFIG_TEMPLATES_CINDER_CEPH[@]} + [designate-k8s]=${CONFIG_TEMPLATES_DESIGNATE[@]} + [designate-bind-k8s]=${NULL_ARRAY[@]} + [glance-k8s]=${CONFIG_TEMPLATES_GLANCE[@]} + [gnocchi-k8s]=${CONFIG_TEMPLATES_GNOCCHI[@]} + [heat-k8s]=${CONFIG_TEMPLATES_HEAT[@]} + [horizon-k8s]=${NULL_ARRAY[@]} + [keystone-k8s]=${CONFIG_TEMPLATES_KEYSTONE[@]} + [keystone-ldap-k8s]=${NULL_ARRAY[@]} + [magnum-k8s]=${CONFIG_TEMPLATES_MAGNUM[@]} + [neutron-k8s]=${CONFIG_TEMPLATES_NEUTRON[@]} + [nova-k8s]=${CONFIG_TEMPLATES_NOVA[@]} + [octavia-k8s]=${CONFIG_TEMPLATES_OCTAVIA[@]} + [openstack-exporter-k8s]=${NULL_ARRAY[@]} + [openstack-hypervisor]=${NULL_ARRAY[@]} + [ovn-central-k8s]=${NULL_ARRAY[@]} + [ovn-relay-k8s]=${NULL_ARRAY[@]} + [placement-k8s]=${CONFIG_TEMPLATES_PLACEMENT[@]} +) + + +function copy_ops_sunbeam { + cp -rf ../../ops-sunbeam/ops_sunbeam lib/ +} + +function copy_internal_libs { + internal_libs_=${INTERNAL_LIBS[$1]} + echo "copy_internal_libs for $1:" + for lib in ${internal_libs_[@]}; do + echo "Copying $lib" + cp -rf ../../libs/internal/lib/charms/$lib lib/charms/ + done +} + +function copy_external_libs { + echo "copy_external_libs for $1:" + external_libs_=${EXTERNAL_LIBS[$1]} + for lib in ${external_libs_[@]}; do + echo "Copying $lib" + cp -rf ../../libs/external/lib/charms/$lib lib/charms/ + done +} + +function copy_config_templates { + echo "copy_config_templates for $1:" + config_templates_=${CONFIG_TEMPLATES[$1]} + for part in ${config_templates_[@]}; do + echo "Copying $part" + cp -rf ../../templates/parts/$part src/templates/parts/ + done +} + +function copy_juju_ignore { + cp ../../.jujuignore . +} + +function copy_stestr_conf { + cp ../../.stestr.conf . +} + +function remove_libs { + rm -rf lib +} + +function remove_templates_parts_dir { + rm -rf src/templates/parts +} + +function remove_juju_ignore { + rm .jujuignore +} + +function remove_stestr_conf { + rm .stestr.conf +} + +function push_common_files { + if [[ $# != 1 ]]; + then + echo "push_common_files: Expected one argument" + exit 1 + fi + + pushd charms/$1 + + mkdir -p lib/charms + mkdir -p src/templates/parts + + copy_ops_sunbeam + copy_internal_libs $1 + copy_external_libs $1 + copy_config_templates $1 + copy_stestr_conf + copy_juju_ignore + + popd +} + +function pop_common_files { + pushd charms/$1 + + remove_libs + remove_templates_parts_dir + remove_stestr_conf + remove_juju_ignore + + popd +} diff --git a/fetch_libs.sh b/fetch_libs.sh new file mode 100755 index 00000000..8521bcc7 --- /dev/null +++ b/fetch_libs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +pushd libs/external + +echo "INFO: Fetching libs from charmhub." +charmcraft fetch-lib charms.data_platform_libs.v0.database_requires +charmcraft fetch-lib charms.grafana_k8s.v0.grafana_auth +charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch +charmcraft fetch-lib charms.operator_libs_linux.v2.snap +charmcraft fetch-lib charms.prometheus_k8s.v0.prometheus_scrape +charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq +charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates +charmcraft fetch-lib charms.traefik_k8s.v2.ingress +charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route +charmcraft fetch-lib charms.vault_k8s.v0.vault_kv + +popd diff --git a/charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/libs/external/lib/charms/data_platform_libs/v0/database_requires.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py rename to libs/external/lib/charms/data_platform_libs/v0/database_requires.py diff --git a/charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py b/libs/external/lib/charms/grafana_agent/v0/cos_agent.py similarity index 100% rename from charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py rename to libs/external/lib/charms/grafana_agent/v0/cos_agent.py diff --git a/charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py b/libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py similarity index 100% rename from charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py rename to libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py diff --git a/charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py similarity index 100% rename from charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py rename to libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py diff --git a/charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py b/libs/external/lib/charms/operator_libs_linux/v2/snap.py similarity index 100% rename from charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py rename to libs/external/lib/charms/operator_libs_linux/v2/snap.py diff --git a/charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py b/libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py similarity index 100% rename from charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py rename to libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py diff --git a/charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py similarity index 100% rename from charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py rename to libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py diff --git a/charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py similarity index 100% rename from charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py rename to libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py diff --git a/charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py b/libs/external/lib/charms/traefik_k8s/v2/ingress.py similarity index 100% rename from charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py rename to libs/external/lib/charms/traefik_k8s/v2/ingress.py diff --git a/charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py b/libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py similarity index 100% rename from charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py rename to libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py diff --git a/charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py b/libs/external/lib/charms/vault_k8s/v0/vault_kv.py similarity index 100% rename from charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py rename to libs/external/lib/charms/vault_k8s/v0/vault_kv.py diff --git a/charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py rename to libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py rename to libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py rename to libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py diff --git a/charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py similarity index 100% rename from charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py rename to libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py diff --git a/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py rename to libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py similarity index 100% rename from charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py rename to libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/libs/internal/lib/charms/keystone_k8s/v0/domain_config.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py rename to libs/internal/lib/charms/keystone_k8s/v0/domain_config.py diff --git a/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py similarity index 100% rename from charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_service.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_service.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v1/identity_service.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py rename to libs/internal/lib/charms/keystone_k8s/v1/identity_service.py diff --git a/charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py similarity index 100% rename from charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py rename to libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py diff --git a/charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py b/libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py similarity index 100% rename from charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py rename to libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py diff --git a/ops-sunbeam/pyproject.toml b/ops-sunbeam/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/ops-sunbeam/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/ops-sunbeam/test-requirements.txt b/ops-sunbeam/test-requirements.txt deleted file mode 100644 index b196466f..00000000 --- a/ops-sunbeam/test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -coverage -mock -stestr -requests -pytest -ops-scenario>=4.0 diff --git a/ops-sunbeam/tox.ini b/ops-sunbeam/tox.ini deleted file mode 100644 index 2cdf6310..00000000 --- a/ops-sunbeam/tox.ini +++ /dev/null @@ -1,135 +0,0 @@ -# Operator charm helper: tox.ini - -[tox] -skipsdist = True -envlist = lint, py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/ops_sunbeam -tst_path = {toxinidir}/tests/unit_tests/ -scenario_tst_path = {toxinidir}/tests/scenario_tests/ -tst_lib_path = {toxinidir}/tests/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -cookie_cutter_path = {toxinidir}/shared_code/sunbeam_charm/\{\{cookiecutter.service_name\}\} -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -install_command = - pip install {opts} {packages} -commands = - stestr run --slowest {posargs} - pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO -allowlist_externals = - git - charmcraft - fetch-libs.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]tst_lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]tst_lib_path} - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:cookie] -basepython = python3 -deps = -r{toxinidir}/cookie-requirements.txt -commands = /bin/true - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]tst_lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]tst_lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]tst_lib_path} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:scenario] -description = Scenario tests -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = - pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - unit_tests/* - -[testenv:venv] -basepython = python3 -commands = {posargs} - -[flake8] -ignore = E226,E402,ANN101,ANN003,W504 diff --git a/playbooks/charm/build.yaml b/playbooks/charm/build.yaml new file mode 100644 index 00000000..e794a3ad --- /dev/null +++ b/playbooks/charm/build.yaml @@ -0,0 +1,6 @@ +- hosts: all + roles: + - ensure-tox + - role: charm-build + vars: + charm_build_name: "{{ charm }}" diff --git a/playbooks/charm/publish.yaml b/playbooks/charm/publish.yaml new file mode 100644 index 00000000..547b9240 --- /dev/null +++ b/playbooks/charm/publish.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - charm-publish diff --git a/playbooks/collect-run-data.yaml b/playbooks/collect-run-data.yaml new file mode 100644 index 00000000..cf81022a --- /dev/null +++ b/playbooks/collect-run-data.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - collect-run-data diff --git a/playbooks/zaza-func-test.yaml b/playbooks/zaza-func-test.yaml new file mode 100644 index 00000000..41e5c315 --- /dev/null +++ b/playbooks/zaza-func-test.yaml @@ -0,0 +1,6 @@ +- hosts: all + roles: + - ensure-tox + - use-docker-mirror + - microk8s-cloud + - zaza-func-test diff --git a/charms/aodh-k8s/pyproject.toml b/pyproject.toml similarity index 100% rename from charms/aodh-k8s/pyproject.toml rename to pyproject.toml diff --git a/render_bundles.py b/render_bundles.py new file mode 100644 index 00000000..4a10bddf --- /dev/null +++ b/render_bundles.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Render all smoke bundles. + +Renders smoke bundles with context of locally built charms. +Prepares the context with assumption the charm is locally +built if corresponding *.charm exists in current folder. + +Assumption: All build charms will be in sunbeam-charms folder. +""" + +import glob +from pathlib import ( + Path, +) + +from jinja2 import ( + Environment, + FileSystemLoader, +) + +test_directories = [ dir_.name for dir_ in list(Path("tests").glob('*')) ] +built_charms = glob.glob("*.charm") +context = { + charm.rstrip(".charm").replace("-", "_"): True for charm in built_charms +} +print(f"Using context: {context}") + +for test_dir in test_directories: + bundle_dir = f"tests/{test_dir}" + template_loader = Environment(loader=FileSystemLoader(bundle_dir)) + bundle_template = template_loader.get_template("smoke.yaml.j2") + smoke_file = Path(f"{bundle_dir}/bundles/smoke.yaml") + smoke_file.parent.mkdir(parents=True, exist_ok=True) + with smoke_file.open("w", encoding="utf-8") as content: + content.write(bundle_template.render(context)) + print(f"Rendered smoke bundle: {smoke_file}") + with smoke_file.open("r", encoding="utf-8") as content: + print(content.read()) diff --git a/roles/charm-build/tasks/main.yaml b/roles/charm-build/tasks/main.yaml new file mode 100644 index 00000000..f4e3bd7e --- /dev/null +++ b/roles/charm-build/tasks/main.yaml @@ -0,0 +1,69 @@ +- name: lxd apt packages are not present + apt: + name: + - lxd + - lxd-client + state: absent + purge: true + become: true + +- name: snapd is installed + apt: + name: snapd + become: true + +- name: lxd snap is installed + snap: + name: lxd + channel: latest/stable + become: true + +- name: lxd is initialised + command: lxd init --auto + become: true + +- name: current user is in lxd group + user: + name: "{{ ansible_user }}" + groups: lxd + append: true + become: true + +- name: reset ssh connection to apply permissions from new group + meta: reset_connection + +- name: charmcraft is installed + snap: + name: charmcraft + channel: "{{ charmcraft_channel | default('latest/stable') }}" + classic: true + become: true + +- name: charm is packed + command: + cmd: "{{ tox_executable }} -e build -- {{ charm_build_name }}" + chdir: "{{ zuul.project.src_dir }}" + register: res + retries: 3 + delay: 30 + until: > + "Charm packed ok" in res.stdout + failed_when: '"Failed instance creation" in res.stdout' + +- name: built charm is available in the zuul log root for auto artifact upload + fetch: + src: "{{ zuul.project.src_dir }}/charms/{{ charm_build_name }}/{{ charm_build_name }}.charm" + dest: "{{ zuul.executor.log_root }}/" + flat: true + become: true + +- name: Upload artifacts + zuul_return: + data: + zuul: + artifacts: + - name: charm + url: "{{ charm_build_name }}.charm" + metadata: + type: charm + name: "{{ charm_build_name }}" diff --git a/roles/charm-publish/defaults/main.yaml b/roles/charm-publish/defaults/main.yaml new file mode 100644 index 00000000..a9306832 --- /dev/null +++ b/roles/charm-publish/defaults/main.yaml @@ -0,0 +1,23 @@ +publish_channels: + keystone-k8s: latest/edge + glance-k8s: latest/edge + nova-k8s: latest/edge + placement-k8s: latest/edge + neutron-k8s: latest/edge + ovn-central-k8s: latest/edge + ovn-relay-k8s: latest/edge + cinder-k8s: latest/edge + cinder-ceph-k8s: latest/edge + horizon-k8s: latest/edge + heat-k8s: latest/edge + octavia-k8s: latest/edge + aodh-k8s: latest/edge + ceilometer-k8s: latest/edge + gnocchi-k8s: latest/edge + barbican-k8s: latest/edge + designate-k8s: latest/edge + designate-bind-k8s: latest/edge + magnum-k8s: latest/edge + keystone-ldap-k8s: latest/edge + openstack-exporter-k8s: latest/edge + openstack-hypervisor: latest/edge diff --git a/roles/charm-publish/tasks/main.yaml b/roles/charm-publish/tasks/main.yaml new file mode 100644 index 00000000..7a851492 --- /dev/null +++ b/roles/charm-publish/tasks/main.yaml @@ -0,0 +1,53 @@ +- name: Get all job names from gate pipeline + uri: + url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}" + register: build_output + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=gate" + +- name: Get relevant charm build jobs + set_fact: + relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list }}" + +- name: Print relevant build jobs + debug: + msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}" + +- name: built charm is present locally (artifact from gate pipeline) + include_role: + name: download-artifact + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_type: charm + download_artifact_pipeline: gate + download_artifact_job: "{{ item }}" + download_artifact_directory: "{{ zuul.project.src_dir }}" + with_items: "{{ relevant_charm_build_jobs }}" + +- name: Get all downloaded charm names + args: + chdir: "{{ zuul.project.src_dir }}" + executable: /bin/bash + shell: | + ls *.charm | cut -d"." -f 1 + register: built_charms + +- name: Prepare charm channel dict for downloaded charms + set_fact: + charm_channels: "{{ charm_channels | default({}) | combine({item.key: item.value}) }}" + loop: "{{ lookup('ansible.builtin.dict', publish_channels) }}" + when: "{{ item.key in built_charms.stdout_lines }}" + +- name: Print charm channel dict + debug: + msg: "Charms to be published: {{ charm_channels }}" + +- name: Publish charms in a loop + include_tasks: "publish.yaml" + vars: + charm_build_name: "{{ channel.key }}" + publish_channel: "{{ channel.value }}" + loop: "{{ charm_channels|dict2items }}" + loop_control: + loop_var: channel diff --git a/roles/charm-publish/tasks/publish.yaml b/roles/charm-publish/tasks/publish.yaml new file mode 100644 index 00000000..7d85d799 --- /dev/null +++ b/roles/charm-publish/tasks/publish.yaml @@ -0,0 +1,59 @@ +- name: Publish charms to charmhub + when: publish_charm + environment: + CHARMCRAFT_AUTH: "{{ charmhub_token.value }}" + block: + - name: Install docker + include_role: + name: ensure-docker + + - name: Upload oci-image to charmhub + register: upload_oci_image_output + vars: + metadata: "{{ lookup('file', zuul.executor.work_root+'/'+zuul.project.src_dir+'/charms/'+charm_build_name+'/metadata.yaml') | from_yaml }}" + args: + executable: /bin/bash + shell: | + set -x + image={{ item.value['upstream-source'] }} + # Remove docker.io/ in the OCI image so that docker pulls image + # from mirror if configured. + image=${image#"docker.io/"} + docker pull $image + digest=`docker inspect --format {% raw %}'{{ index .RepoDigests 0 }}' {% endraw %} $image` + charmcraft upload-resource {{ charm_build_name }} {{ item.key }} --image $digest + retries: 3 + until: > + ("Revision" in upload_oci_image_output.stdout) + loop: "{{ lookup('ansible.builtin.dict', metadata.resources|default({}), wantlist=True) }}" + when: "item.value.type == 'oci-image'" + + - name: Extract Resource revisions + set_fact: + resource_revision_flags: "{{ resource_revision_flags | default('') + ' --resource ' + item.item.key + ':' + (item.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first) }}" + with_items: "{{ upload_oci_image_output.results }}" + + - name: Upload charm to charmhub + register: upload_charm_output + args: + chdir: "{{ zuul.project.src_dir }}" + # TODO: The below command can error out with a message that says + # upload with that digest already exists. This case need to be handled. + # More details https://github.com/canonical/charmcraft/issues/826 + command: + charmcraft upload -v --name {{ charm_build_name }} {{ charm_build_name }}.charm + retries: 3 + until: > + ("Revision" in upload_charm_output.stdout) + + - name: Extract Charm revision + set_fact: + charm_revision: "{{ upload_charm_output.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first }}" + + - name: Release charm + register: release_charm_output + command: + charmcraft release {{ charm_build_name }} --revision {{ charm_revision }} --channel {{ publish_channel }} {{ resource_revision_flags | default("") }} + retries: 3 + until: > + ("Revision" in release_charm_output.stdout) diff --git a/roles/collect-run-data/tasks/main.yaml b/roles/collect-run-data/tasks/main.yaml new file mode 100644 index 00000000..81123bc5 --- /dev/null +++ b/roles/collect-run-data/tasks/main.yaml @@ -0,0 +1,95 @@ +- name: test runner packages are installed + apt: + name: + - jq + become: true +- name: Create destination for logs + file: + path: "{{ zuul.project.src_dir }}/log" + state: directory + mode: 0755 +- name: collect microk8s inspection report + args: + executable: /bin/bash + shell: | + cp /var/snap/microk8s/current/inspection-report-*.tar.gz "{{ zuul.project.src_dir }}/log/" + failed_when: false +- name: debug logs replay + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')" + juju switch $MODEL + juju debug-log --replay > {{ zuul.project.src_dir }}/log/debug-hooks.txt + exit 0 +- name: debug describe pods + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')" + microk8s.kubectl describe -n $MODEL pods > {{ zuul.project.src_dir }}/log/describe-pods.txt + CONTROLLER_MODEL="$(microk8s.kubectl get ns | grep controller | awk '{print $1}')" + microk8s.kubectl describe -n $CONTROLLER_MODEL pods > {{ zuul.project.src_dir }}/log/describe-controller-pods.txt + exit 0 +- name: juju status + args: + executable: /bin/bash + shell: | + set -o pipefail + for model in $(juju models | grep zaza- | awk '{gsub(/\*?/,""); print $1}'); do + juju status -m $model > {{ zuul.project.src_dir }}/log/juju-status.$model.txt + juju status -m $model --format=yaml > {{ zuul.project.src_dir }}/log/juju-status.$model.yaml + done +- name: Collect var logs + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + UNITS=$(juju status --format oneline | awk '{print $2}' | sed -e 's!:!!' | grep -Ev '^$' | paste -s -d' ') + for UNIT_NAME in $UNITS; do + POD_NAME=$(echo $UNIT_NAME | sed -e 's!/!-!') + CONTAINERS=$(microk8s.kubectl get pods -n $MODEL_NAME $POD_NAME -o jsonpath='{.spec.containers[*].name}' | sed -e 's/charm //') + for CONTAINER in $CONTAINERS; do + juju ssh --container $CONTAINER -m $MODEL_NAME $UNIT_NAME "tar zcf /tmp/logs.tgz /var/log/" + juju scp --container $CONTAINER -m $MODEL_NAME $UNIT_NAME:/tmp/logs.tgz {{ zuul.project.src_dir }}/log/$POD_NAME-$CONTAINER.tgz + done + done +- name: Collect pods logs + args: + executable: /bin/bash + shell: | + set -o pipefail + LOG_FOLDER={{ zuul.project.src_dir }}/log/pods/ + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + mkdir -p $LOG_FOLDER + for pod in $(microk8s.kubectl get pods -n $MODEL_NAME -o=jsonpath='{.items[*].metadata.name}'); + do + echo Collecting logs: $pod + microk8s.kubectl logs --ignore-errors -n $MODEL_NAME --all-containers $pod > $LOG_FOLDER/$pod.log + done +- name: Collect units' info + args: + executable: /bin/bash + shell: | + set -o pipefail + set -x + LOG_FOLDER={{ zuul.project.src_dir }}/log/unit-info/ + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + mkdir -p $LOG_FOLDER + for unit in $(juju status --format json | jq -r '[.applications[].units | keys[0]] | join("\n")'); + do + echo Collecting unit info: $unit + unit_name=$(echo $unit | tr / -) + juju show-unit --output="$LOG_FOLDER/$unit_name.yaml" $unit + done +- name: fetch juju logs + synchronize: + dest: "{{ zuul.executor.log_root }}" + mode: pull + src: "{{ zuul.project.src_dir }}/log" + verify_host: true + owner: false + group: false diff --git a/roles/microk8s-cloud/tasks/main.yaml b/roles/microk8s-cloud/tasks/main.yaml new file mode 100644 index 00000000..60b29b36 --- /dev/null +++ b/roles/microk8s-cloud/tasks/main.yaml @@ -0,0 +1,157 @@ +- name: snapd is installed + apt: + name: snapd + become: true + +- name: set microk8s related variables + set_fact: + microk8s_group: "{{ 'microk8s' if microk8s_classic_mode | default(true) else 'snap_microk8s' }}" + microk8s_command_escalation: "{{ false if microk8s_classic_mode | default(true) else true }}" + +- name: Disable ipv6 + become: true + sysctl: + name: "net.ipv6.conf.all.disable_ipv6" + value: "1" + state: "present" + reload: "yes" + +- name: microk8s is installed + snap: + name: microk8s + classic: "{{ microk8s_classic_mode | default(true) }}" + channel: "{{ microk8s_channel | default('latest/stable') }}" + become: true + +- name: current user is in microk8s group + user: + name: "{{ ansible_user }}" + groups: "{{ microk8s_group }}" + append: true + become: true + +- name: reset ssh connection to apply permissions from new group + meta: reset_connection + +- name: microk8s status + block: + - name: microk8s status + command: + cmd: microk8s status --wait-ready --timeout 300 + rescue: + - name: microk8s inspect + command: + cmd: microk8s inspect + become: "{{ microk8s_command_escalation }}" + - name: microk8s status + command: + # second chance to get status + cmd: microk8s status + +- name: Create docker.io certs dir + when: + - docker_mirror is defined + file: + path: /var/snap/microk8s/current/args/certs.d/docker.io + state: directory + owner: root + group: "{{ microk8s_group }}" + mode: '0770' + +- name: Render microk8s registry mirror template + when: + - docker_mirror is defined + template: + src: hosts.j2 + dest: /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml + group: "{{ microk8s_group }}" + vars: + mirror_location: "{{ docker_mirror }}" + server: https://docker.io + +- name: Check docker.io hosts.toml + when: + - docker_mirror is defined + command: + cmd: cat /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml + +- name: microk8s is started + command: + cmd: microk8s start + become: "{{ microk8s_command_escalation }}" + +- name: microk8s is running and ready + command: + cmd: microk8s status --wait-ready + register: res + failed_when: '"is running" not in res.stdout' + +- name: microk8s dns addon is enabled + command: + cmd: microk8s enable dns + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s hostpath storage addon is enabled + command: + cmd: microk8s enable hostpath-storage + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s metallb addon is enabled + command: + # ip range is an arbitrary choice; may need to be changed later + cmd: microk8s enable metallb:10.170.0.1-10.170.0.100 + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s addons are ready + command: + cmd: microk8s status --format short + register: res + retries: 18 + delay: 10 # 18 * 10 = 3 minutes + until: > + "core/dns: enabled" in res.stdout and + "core/hostpath-storage: enabled" in res.stdout and + "core/metallb: enabled" in res.stdout + changed_when: res.attempts > 1 + +- name: juju is installed + snap: + name: juju + classic: "{{ juju_classic_mode | default(true) }}" + channel: "{{ juju_channel | default('latest/stable') }}" + become: true + +- name: Ensure ~/.local/share directory exist + file: + path: ~/.local/share + state: directory + +- name: juju is bootstrapped on microk8s + command: + cmd: juju bootstrap --config bootstrap-timeout=600 microk8s microk8s + register: res + retries: 3 + delay: 10 + until: > + "Bootstrap complete" in res.stderr or + "already exists" in res.stderr + failed_when: '"ERROR" in res.stderr and "already exists" not in res.stderr' + +- name: current juju controller is microk8s + command: + cmd: juju switch microk8s + register: res + changed_when: '"no change" not in res.stderr' + +- name: Collect snap versions + command: snap list + register: snap_out + +- name: Show snap versions + debug: msg="{{ snap_out.stdout }}" diff --git a/roles/microk8s-cloud/templates/hosts.j2 b/roles/microk8s-cloud/templates/hosts.j2 new file mode 100644 index 00000000..a86649af --- /dev/null +++ b/roles/microk8s-cloud/templates/hosts.j2 @@ -0,0 +1,4 @@ +server = "{{ server }}" + +[host."{{ mirror_location }}"] + capabilities = ["pull", "resolve"] diff --git a/roles/zaza-func-test/tasks/main.yaml b/roles/zaza-func-test/tasks/main.yaml new file mode 100644 index 00000000..053d87de --- /dev/null +++ b/roles/zaza-func-test/tasks/main.yaml @@ -0,0 +1,31 @@ +- name: Get all job names + uri: + url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}" + register: build_output + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=check" + +- name: Get relevant charm build jobs + set_fact: + relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list | intersect(charm_jobs) }}" + +- name: Print relevant build jobs + debug: + msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}" + +- name: built charm is present locally (artifact from previous job) + include_role: + name: download-artifact + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_type: charm + download_artifact_pipeline: check + download_artifact_job: "{{ item }}" + download_artifact_directory: "{{ zuul.project.src_dir }}" + with_items: "{{ relevant_charm_build_jobs }}" + +- name: run smoke tests + command: + cmd: "{{ tox_executable }} -e func -- --smoke --test-directory={{ test_dir }}" + chdir: "{{ zuul.project.src_dir }}" diff --git a/run_tox.sh b/run_tox.sh new file mode 100755 index 00000000..a82cf957 --- /dev/null +++ b/run_tox.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +source common.sh + +if [[ $1 == "fmt" ]]; +then + src_path_array=$(ls -d -1 "charms/"**/src) + tst_path_array=$(ls -d -1 "charms/"**/tests) + lib_path_array=$(ls -d -1 "charms/"**/lib) + + src_path="${src_path_array[*]}" + tst_path="${tst_path_array[*]}" + lib_path="${lib_path_array[*]}" + + isort ${src_path} ${tst_path} + black --config pyproject.toml ${src_path} ${tst_path} +elif [[ $1 == "pep8" ]]; +then + src_path_array=$(ls -d -1 "charms/"**/src) + tst_path_array=$(ls -d -1 "charms/"**/tests) + + src_path="${src_path_array[*]}" + tst_path="${tst_path_array[*]}" + + codespell ${src_path} ${tst_path} + pflake8 --config pyproject.toml ${src_path} ${tst_path} + isort --check-only --diff ${src_path} ${tst_path} + black --config pyproject.toml --check --diff ${src_path} ${tst_path} +elif [[ $1 =~ ^(py3|py310|py311)$ ]]; +then + # Run py3 on ops-sunbeam + pushd ops-sunbeam + stestr run --slowest || exit 1 + popd + + # Run py3 on all sunbeam charms + charms=($(ls charms)) + for charm in ${charms[@]}; do + push_common_files $charm || exit 1 + pushd charms/$charm + PYTHONPATH=./src:./lib stestr run --slowest || exit 1 + popd + pop_common_files $charm || exit 1 + done +elif [[ $1 == "cover" ]]; +then + coverage erase + + # Run coverage on ops-sunbeam + pushd ops-sunbeam + coverage erase + PYTHON="coverage run --omit .tox/*,tests/*" stestr run --slowest || exit 1 + coverage combine + popd + + # Run coverage on all sunbeam charms + charms=($(ls charms)) + for charm in ${charms[@]}; do + push_common_files $charm || exit 1 + pushd charms/$charm + coverage erase + PYTHONPATH=./src:./lib:../../ops-sunbeam PYTHON="coverage run --omit .tox/*,tests/*,src/templates/*" stestr run --slowest || exit 1 + coverage combine + popd + done + + # Prepare coverage report + coverage combine charms/*/.coverage ops-sunbeam/.coverage + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + + # Common files should be deleted after coverage combine + for charm in ${charms[@]}; do + pop_common_files $charm || exit 1 + done +elif [[ $1 == "build" ]]; +then + if [[ $# != 2 ]]; + then + echo "Command format: tox -e build <charm>" + exit 1 + fi + + charm=$2 + charms=($(ls charms)) + if [[ ! ${charms[@]} =~ $charm ]]; + then + echo "Argument should be one of ${charms[@]}"; + exit 1 + fi + + push_common_files $charm || exit 1 + + pushd charms/$charm + charmcraft -v pack || exit 1 + if [[ -e "${charm}.charm" ]]; + then + echo "Removing bad downloaded charm maybe?" + rm "${charm}.charm" + fi + echo "Renaming charm ${charm}_*.charm to ${charm}.charm" + mv ${charm}_*.charm ${charm}.charm + popd + + pop_common_files $charm || exit 1 +else + echo "tox argument should be one of pep8, py3, py310, py311, cover"; + exit 1 +fi diff --git a/charms/aodh-k8s/src/templates/parts/database-connection b/templates/parts/database-connection similarity index 55% rename from charms/aodh-k8s/src/templates/parts/database-connection rename to templates/parts/database-connection index 1fd70ce2..21c25eb6 100644 --- a/charms/aodh-k8s/src/templates/parts/database-connection +++ b/templates/parts/database-connection @@ -1,3 +1,5 @@ {% if database.connection -%} connection = {{ database.connection }} +{% else -%} +connection = sqlite:////var/lib/openstack/openstack.db {% endif -%} diff --git a/charms/aodh-k8s/src/templates/parts/identity-data b/templates/parts/identity-data similarity index 100% rename from charms/aodh-k8s/src/templates/parts/identity-data rename to templates/parts/identity-data diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds b/templates/parts/identity-data-id-creds similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds rename to templates/parts/identity-data-id-creds diff --git a/charms/magnum-k8s/src/templates/parts/section-certificates b/templates/parts/section-certificates similarity index 100% rename from charms/magnum-k8s/src/templates/parts/section-certificates rename to templates/parts/section-certificates diff --git a/charms/aodh-k8s/src/templates/parts/section-database b/templates/parts/section-database similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-database rename to templates/parts/section-database diff --git a/charms/aodh-k8s/src/templates/parts/section-federation b/templates/parts/section-federation similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-federation rename to templates/parts/section-federation diff --git a/charms/aodh-k8s/src/templates/parts/section-identity b/templates/parts/section-identity similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-identity rename to templates/parts/section-identity diff --git a/charms/aodh-k8s/src/templates/parts/section-middleware b/templates/parts/section-middleware similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-middleware rename to templates/parts/section-middleware diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-cache b/templates/parts/section-oslo-cache similarity index 100% rename from charms/keystone-k8s/src/templates/parts/section-oslo-cache rename to templates/parts/section-oslo-cache diff --git a/charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit b/templates/parts/section-oslo-messaging-rabbit similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit rename to templates/parts/section-oslo-messaging-rabbit diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-middleware b/templates/parts/section-oslo-middleware similarity index 100% rename from charms/keystone-k8s/src/templates/parts/section-oslo-middleware rename to templates/parts/section-oslo-middleware diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications b/templates/parts/section-oslo-notifications similarity index 100% rename from charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications rename to templates/parts/section-oslo-notifications diff --git a/charms/aodh-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-service-credentials rename to templates/parts/section-service-credentials diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials-from-identity-service similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/section-service-credentials rename to templates/parts/section-service-credentials-from-identity-service diff --git a/charms/barbican-k8s/src/templates/parts/section-service-user b/templates/parts/section-service-user similarity index 100% rename from charms/barbican-k8s/src/templates/parts/section-service-user rename to templates/parts/section-service-user diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds b/templates/parts/section-service-user-from-identity-credentials similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds rename to templates/parts/section-service-user-from-identity-credentials diff --git a/charms/aodh-k8s/src/templates/parts/section-signing b/templates/parts/section-signing similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-signing rename to templates/parts/section-signing diff --git a/charms/magnum-k8s/src/templates/parts/section-trust b/templates/parts/section-trust similarity index 100% rename from charms/magnum-k8s/src/templates/parts/section-trust rename to templates/parts/section-trust diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..4cdc9db0 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,17 @@ +mock +stestr +coverage + +tenacity # ops-sunbeam +ops # all charms +lightkube # almost all charms +pwgen # keystone-k8s +python-keystoneclient # keystone-k8s +cryptography # neutron-k8s +jsonschema # neutron-k8s +pytest-interface-tester # barbican-k8s +requests # cinder-ceph-k8s +netifaces # cinder-ceph-k8s +cosl # openstack-exporter +git+https://github.com/juju/charm-helpers.git#egg=charmhelpers # cinder-ceph-k8s,glance-k8s,gnocchi-k8s +git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # cinder-ceph-k8s diff --git a/tests/caas/smoke.yaml.j2 b/tests/caas/smoke.yaml.j2 new file mode 100644 index 00000000..9a26899b --- /dev/null +++ b/tests/caas/smoke.yaml.j2 @@ -0,0 +1,190 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + vault: + charm: ch:vault-k8s + channel: latest/edge + scale: 1 + trust: false + tls-operator: + charm: self-signed-certificates + channel: latest/beta + scale: 1 + options: + ca-common-name: internal-ca + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + ovn-central: + {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%} + charm: ../../../ovn-central-k8s.charm + {% else -%} + charm: ch:ovn-central-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + glance: + {% if glance_k8s is defined and glance_k8s is sameas true -%} + charm: ../../../glance-k8s.charm + {% else -%} + charm: ch:glance-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + storage: + local-repository: 5G + resources: + glance-api-image: ghcr.io/canonical/glance-api:2023.2 + heat: + {% if heat_k8s is defined and heat_k8s is sameas true -%} + charm: ../../../heat-k8s.charm + {% else -%} + charm: ch:heat-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2 + heat-engine-image: ghcr.io/canonical/heat-consolidated:2023.2 + octavia: + {% if octavia_k8s is defined and octavia_k8s is sameas true -%} + charm: ../../../octavia-k8s.charm + {% else -%} + charm: ch:octavia-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + octavia-api-image: ghcr.io/canonical/octavia-consolidated:2023.2 + octavia-driver-agent-image: ghcr.io/canonical/octavia-consolidated:2023.2 + octavia-housekeeping-image: ghcr.io/canonical/octavia-consolidated:2023.2 + barbican: + {% if barbican_k8s is defined and barbican_k8s is sameas true -%} + charm: ../../../barbican-k8s.charm + {% else -%} + charm: ch:barbican-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + barbican-api-image: ghcr.io/canonical/barbican-consolidated:2023.2 + barbican-worker-image: ghcr.io/canonical/barbican-consolidated:2023.2 + magnum: + {% if magnum_k8s is defined and magnum_k8s is sameas true -%} + charm: ../../../magnum-k8s.charm + {% else -%} + charm: ch:magnum-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + magnum-api-image: ghcr.io/canonical/magnum-consolidated:2023.2 + magnum-conductor-image: ghcr.io/canonical/magnum-consolidated:2023.2 + +relations: +- - tls-operator:certificates + - ovn-central:certificates + +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - glance:database +- - keystone:identity-service + - glance:identity-service +- - rabbitmq:amqp + - glance:amqp +- - traefik:ingress + - glance:ingress-public + +- - mysql:database + - heat:database +- - keystone:identity-service + - heat:identity-service +- - keystone:identity-ops + - heat:identity-ops +- - traefik:traefik-route + - heat:traefik-route-public +- - rabbitmq:amqp + - heat:amqp + +- - mysql:database + - octavia:database +- - keystone:identity-service + - octavia:identity-service +- - keystone:identity-ops + - octavia:identity-ops +- - traefik:ingress + - octavia:ingress-public +- - tls-operator:certificates + - octavia:certificates +- - octavia:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - barbican:database +- - rabbitmq:amqp + - barbican:amqp +- - keystone:identity-service + - barbican:identity-service +- - keystone:identity-ops + - barbican:identity-ops +- - traefik:ingress + - barbican:ingress-public +- - vault:vault-kv + - barbican:vault-kv + +- - mysql:database + - magnum:database +- - rabbitmq:amqp + - magnum:amqp +- - keystone:identity-service + - magnum:identity-service +- - keystone:identity-ops + - magnum:identity-ops +- - traefik:ingress + - magnum:ingress-public diff --git a/tests/caas/tests.yaml b/tests/caas/tests.yaml new file mode 100644 index 00000000..4c2cd46b --- /dev/null +++ b/tests/caas/tests.yaml @@ -0,0 +1,56 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles +tests: + - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + vault: + workload-status: active + workload-status-message-regex: '^$' + tls-operator: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + ovn-central: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + glance: + workload-status: active + workload-status-message-regex: '^$' + heat: + workload-status: active + workload-status-message-regex: '^.*$' + octavia: + workload-status: active + workload-status-message-regex: '^$' + barbican: + workload-status: active + workload-status-message-regex: '^$' + magnum: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tests/ceph/smoke.yaml.j2 b/tests/ceph/smoke.yaml.j2 new file mode 100644 index 00000000..82fff4cc --- /dev/null +++ b/tests/ceph/smoke.yaml.j2 @@ -0,0 +1,145 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + cinder: + {% if cinder_k8s is defined and cinder_k8s is sameas true -%} + charm: ../../../cinder-k8s.charm + {% else -%} + charm: ch:cinder-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + cinder-api-image: ghcr.io/canonical/cinder-consolidated:2023.2 + cinder-scheduler-image: ghcr.io/canonical/cinder-consolidated:2023.2 + cinder-ceph: + {% if cinder_ceph_k8s is defined and cinder_ceph_k8s is sameas true -%} + charm: ../../../cinder-ceph-k8s.charm + {% else -%} + charm: ch:cinder-ceph-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + cinder-volume-image: ghcr.io/canonical/cinder-consolidated:2023.2 + gnocchi: + {% if gnocchi_k8s is defined and gnocchi_k8s is sameas true -%} + charm: ../../../gnocchi-k8s.charm + {% else -%} + charm: ch:gnocchi-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + gnocchi-api-image: ghcr.io/canonical/gnocchi-consolidated:2023.1 + gnocchi-metricd-image: ghcr.io/canonical/gnocchi-consolidated:2023.1 + ceilometer: + {% if ceilometer_k8s is defined and ceilometer_k8s is sameas true -%} + charm: ../../../ceilometer-k8s.charm + {% else -%} + charm: ch:ceilometer-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + ceilometer-central-image: ghcr.io/canonical/ceilometer-consolidated:2023.2 + ceilometer-notification-image: ghcr.io/canonical/ceilometer-consolidated:2023.2 + aodh: + {% if aodh_k8s is defined and aodh_k8s is sameas true -%} + charm: ../../../aodh-k8s.charm + {% else -%} + charm: ch:aodh-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + aodh-api-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-evaluator-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-notifier-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-listener-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-expirer-image: ghcr.io/canonical/aodh-consolidated:2023.2 + +relations: +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - cinder:database +- - cinder:amqp + - rabbitmq:amqp +- - keystone:identity-service + - cinder:identity-service +- - traefik:ingress + - cinder:ingress-public + +- - cinder-ceph:database + - mysql:database +- - cinder-ceph:amqp + - rabbitmq:amqp +- - cinder:storage-backend + - cinder-ceph:storage-backend + +- - mysql:database + - gnocchi:database +- - traefik:ingress + - gnocchi:ingress-public +- - keystone:identity-service + - gnocchi:identity-service + +- - rabbitmq:amqp + - ceilometer:amqp +- - keystone:identity-credentials + - ceilometer:identity-credentials +- - gnocchi:gnocchi-service + - ceilometer:gnocchi-db + +- - mysql:database + - aodh:database +- - rabbitmq:amqp + - aodh:amqp +- - keystone:identity-service + - aodh:identity-service +- - traefik:ingress + - aodh:ingress-public diff --git a/tests/ceph/tests.yaml b/tests/ceph/tests.yaml new file mode 100644 index 00000000..caec8bdc --- /dev/null +++ b/tests/ceph/tests.yaml @@ -0,0 +1,43 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +# There is no storage provider at the moment so cannot run tests. +configure: + - zaza.charm_tests.noop.setup.basic_setup +tests: + - zaza.charm_tests.noop.tests.NoopTest +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + cinder: + workload-status: active + workload-status-message-regex: '^$' + cinder-ceph: + workload-status: blocked + workload-status-message-regex: '^.*ceph.*$' + ceilometer: + workload-status: waiting + workload-status-message-regex: '^.*Not all relations are ready$' + aodh: + workload-status: active + workload-status-message-regex: '^.*$' + gnocchi: + workload-status: blocked + workload-status-message-regex: '^.*ceph.*$' diff --git a/tests/core/smoke.yaml.j2 b/tests/core/smoke.yaml.j2 new file mode 100644 index 00000000..4598ddf3 --- /dev/null +++ b/tests/core/smoke.yaml.j2 @@ -0,0 +1,192 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + tls-operator: + charm: self-signed-certificates + channel: latest/beta + scale: 1 + options: + ca-common-name: internal-ca + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + ovn-central: + {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%} + charm: ../../../ovn-central-k8s.charm + {% else -%} + charm: ch:ovn-central-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-relay: + {% if ovn_relay_k8s is defined and ovn_relay_k8s is sameas true -%} + charm: ../../../ovn-relay-k8s.charm + {% else -%} + charm: ch:ovn-relay-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + glance: + {% if glance_k8s is defined and glance_k8s is sameas true -%} + charm: ../../../glance-k8s.charm + {% else -%} + charm: ch:glance-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + storage: + local-repository: 5G + resources: + glance-api-image: ghcr.io/canonical/glance-api:2023.2 + nova: + {% if nova_k8s is defined and nova_k8s is sameas true -%} + charm: ../../../nova-k8s.charm + {% else -%} + charm: ch:nova-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + nova-api-image: ghcr.io/canonical/nova-consolidated:2023.2 + nova-scheduler-image: ghcr.io/canonical/nova-consolidated:2023.2 + nova-conductor-image: ghcr.io/canonical/nova-consolidated:2023.2 + placement: + {% if placement_k8s is defined and placement_k8s is sameas true -%} + charm: ../../../placement-k8s.charm + {% else -%} + charm: ch:placement-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + placement-api-image: ghcr.io/canonical/placement-api:2023.2 + neutron: + {% if neutron_k8s is defined and neutron_k8s is sameas true -%} + charm: ../../../neutron-k8s.charm + {% else -%} + charm: ch:neutron-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + debug: true + resources: + neutron-server-image: ghcr.io/canonical/neutron-server:2023.2 + horizon: + {% if horizon_k8s is defined and horizon_k8s is sameas true -%} + charm: ../../../horizon-k8s.charm + {% else -%} + charm: ch:horizon-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + horizon-image: ghcr.io/canonical/horizon:2023.2 + +relations: +- - tls-operator:certificates + - ovn-central:certificates + +- - tls-operator:certificates + - ovn-relay:certificates +- - ovn-relay:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - glance:database +- - keystone:identity-service + - glance:identity-service +- - rabbitmq:amqp + - glance:amqp +- - traefik:ingress + - glance:ingress-public + +- - mysql:database + - nova:database +- - mysql:database + - nova:api-database +- - mysql:database + - nova:cell-database +- - rabbitmq:amqp + - nova:amqp +- - keystone:identity-service + - nova:identity-service +- - traefik:ingress + - nova:ingress-public + +- - mysql:database + - placement:database +- - keystone:identity-service + - placement:identity-service +- - traefik:ingress + - placement:ingress-public + +- - mysql:database + - neutron:database +- - rabbitmq:amqp + - neutron:amqp +- - keystone:identity-service + - neutron:identity-service +- - traefik:ingress + - neutron:ingress-public +- - tls-operator:certificates + - neutron:certificates +- - neutron:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - horizon:database +- - keystone:identity-credentials + - horizon:identity-credentials +- - traefik:ingress + - horizon:ingress-public diff --git a/tests/core/tests.yaml b/tests/core/tests.yaml new file mode 100644 index 00000000..821a7d54 --- /dev/null +++ b/tests/core/tests.yaml @@ -0,0 +1,72 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles + - zaza.openstack.charm_tests.nova.setup.create_flavors + - zaza.openstack.charm_tests.nova.setup.manage_ssh_key +tests: + - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + exclude-list: + - "tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file" + - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create" + - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list" + - "tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete" + - "tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON" + - "tempest.api.compute.servers.test_create_server.ServersTestManualDisk" + - "tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON" + - "tempest.api.compute.servers.test_create_server.ServersTestJSON" + - "tempest.scenario.test_server_multinode.TestServerMultinode.test_schedule_to_all_nodes" + - "tempest.scenario.test_server_basic_ops.TestServerBasicOps.test_server_basic_ops" + - "tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesUnderV243Test.test_add_remove_fixed_ip" + include-list: + - "tempest.api.identity.v3.test_application_credentials.ApplicationCredentialsV3Test.test_create_application_credential" + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + tls-operator: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + ovn-central: + workload-status: active + workload-status-message-regex: '^$' + ovn-relay: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + glance: + workload-status: active + workload-status-message-regex: '^$' + nova: + workload-status: active + workload-status-message-regex: '^$' + placement: + workload-status: active + workload-status-message-regex: '^$' + neutron: + workload-status: active + workload-status-message-regex: '^$' + horizon: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tests/misc/smoke.yaml.j2 b/tests/misc/smoke.yaml.j2 new file mode 100644 index 00000000..6ae7253a --- /dev/null +++ b/tests/misc/smoke.yaml.j2 @@ -0,0 +1,105 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + ldap-server: + charm: ch:ldap-test-fixture-k8s + channel: edge + scale: 1 + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + designate-bind: + {% if designate_bind_k8s is defined and designate_bind_k8s is sameas true -%} + charm: ../../../designate-bind-k8s.charm + {% else -%} + charm: ch:designate-bind-k8s + channel: 9/edge + {% endif -%} + scale: 1 + trust: false + resources: + designate-bind-image: ubuntu/bind9:9.18-22.04_beta + designate: + {% if designate_k8s is defined and designate_k8s is sameas true -%} + charm: ../../../designate-k8s.charm + {% else -%} + charm: ch:designate-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + designate-image: ghcr.io/canonical/designate-consolidated:2023.2 + keystone-ldap: + {% if keystone_ldap_k8s is defined and keystone_ldap_k8s is sameas true -%} + charm: ../../../keystone-ldap-k8s.charm + {% else -%} + charm: ch:keystone-ldap-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + openstack-exporter: + {% if openstack_exporter_k8s is defined and openstack_exporter_k8s is sameas true -%} + charm: ../../../openstack-exporter-k8s.charm + {% else -%} + charm: ch:openstack-exporter-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + resources: + openstack-exporter-image: ghcr.io/canonical/openstack-exporter:1.6.0-7533071 + +relations: +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - designate:database +- - rabbitmq:amqp + - designate:amqp +- - keystone:identity-service + - designate:identity-service +- - traefik:ingress + - designate:ingress-public +- - designate-bind:dns-backend + - designate:dns-backend + +- - keystone:domain-config + - keystone-ldap:domain-config + +- - keystone:identity-ops + - openstack-exporter:identity-ops diff --git a/tests/misc/tests.yaml b/tests/misc/tests.yaml new file mode 100644 index 00000000..647232b4 --- /dev/null +++ b/tests/misc/tests.yaml @@ -0,0 +1,52 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.charm_tests.noop.setup.basic_setup + # https://bugs.launchpad.net/snap-openstack/+bug/2045206 + # - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints +tests: + - zaza.charm_tests.noop.tests.NoopTest + # Tests commented until bug fix for https://bugs.launchpad.net/snap-openstack/+bug/2045206 + # - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal + # - zaza.openstack.charm_tests.keystone.tests_ldap_k8s.LdapExplicitCharmConfigTestsK8S + # - zaza.openstack.charm_tests.openstack_exporter.tests.OpenstackExporterTest +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + ldap-server: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: waiting + workload-status-message-regex: '^.*domain-config.*integration incomplete.*$|^$' + designate-bind: + workload-status: active + workload-status-message-regex: '^.*$' + designate: + workload-status: active + workload-status-message-regex: '^.*$' + keystone-ldap: + workload-status: active + workload-status-message-regex: '^$' + openstack-exporter: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..2d2868ac --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +# Global tox file + +# This file is used to invoke tox in individual charms + +[tox] +skipsdist = True +envlist = pep8,py3 +sitepackages = False +skip_missing_interpreters = False +minversion = 3.18.0 + +[testenv] +passenv = + HOME +allowlist_externals = + {toxinidir}/run_tox.sh + {toxinidir}/fetch_libs.sh + +[testenv:fetch] +basepython = python3 +deps = +commands = + {toxinidir}/fetch_libs.sh + +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + {toxinidir}/run_tox.sh fmt + +[testenv:pep8] +description = Alias for lint +deps = + black + flake8<6 + flake8-docstrings + flake8-copyright + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell +commands = + {toxinidir}/run_tox.sh pep8 + +[testenv:py3] +deps = + -r{toxinidir}/test-requirements.txt +commands = + {toxinidir}/run_tox.sh py3 + +[testenv:py310] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh py310 + +[testenv:py311] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh py311 + +[testenv:cover] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh cover + +[testenv:build] +basepython = python3 +deps = +commands = + {toxinidir}/run_tox.sh build {posargs} + +[testenv:func-noop] +basepython = python3 +deps = + git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza + git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack + git+https://opendev.org/openstack/tempest.git#egg=tempest +commands = + functest-run-suite --help + +[testenv:func] +basepython = python3 +deps = {[testenv:func-noop]deps} +setenv = + TEST_MODEL_SETTINGS = automatically-retry-hooks=true + TEST_MAX_RESOLVE_COUNT = 5 +commands = + python3 render_bundles.py + # Example: functest-run-suite --keep-model --smoke --test-directory=tests/set1 + functest-run-suite --keep-model {posargs} diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml new file mode 100644 index 00000000..648e8a3c --- /dev/null +++ b/zuul.d/jobs.yaml @@ -0,0 +1,410 @@ +- job: + name: charm-build-keystone-k8s + description: Build sunbeam keystone-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-k8s/* + vars: + charm: keystone-k8s +- job: + name: charm-build-glance-k8s + description: Build sunbeam glance-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/glance-k8s/* + vars: + charm: glance-k8s +- job: + name: charm-build-nova-k8s + description: Build sunbeam nova-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/nova-k8s/* + vars: + charm: nova-k8s +- job: + name: charm-build-placement-k8s + description: Build sunbeam placement-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/placement-k8s/* + vars: + charm: placement-k8s +- job: + name: charm-build-neutron-k8s + description: Build sunbeam neutron-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/neutron-k8s/* + vars: + charm: neutron-k8s +- job: + name: charm-build-ovn-central-k8s + description: Build sunbeam ovn-central-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ovn-central-k8s/* + vars: + charm: ovn-central-k8s +- job: + name: charm-build-ovn-relay-k8s + description: Build sunbeam ovn-relay-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ovn-relay-k8s/* + vars: + charm: ovn-relay-k8s +- job: + name: charm-build-cinder-k8s + description: Build sunbeam cinder-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-k8s/* + vars: + charm: cinder-k8s +- job: + name: charm-build-cinder-ceph-k8s + description: Build sunbeam cinder-ceph-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-ceph-k8s/* + vars: + charm: cinder-ceph-k8s +- job: + name: charm-build-horizon-k8s + description: Build sunbeam horizon-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/horizon-k8s/* + vars: + charm: horizon-k8s +- job: + name: charm-build-heat-k8s + description: Build sunbeam heat-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/heat-k8s/* + vars: + charm: heat-k8s +- job: + name: charm-build-octavia-k8s + description: Build sunbeam octavia-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/octavia-k8s/* + vars: + charm: octavia-k8s +- job: + name: charm-build-aodh-k8s + description: Build sunbeam aodh-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/aodh-k8s/* + vars: + charm: aodh-k8s +- job: + name: charm-build-ceilometer-k8s + description: Build sunbeam ceilometer-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ceilometer-k8s/* + vars: + charm: ceilometer-k8s +- job: + name: charm-build-gnocchi-k8s + description: Build sunbeam gnocchi-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/gnocchi-k8s/* + vars: + charm: gnocchi-k8s +- job: + name: charm-build-barbican-k8s + description: Build sunbeam barbican-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/barbican-k8s/* + vars: + charm: barbican-k8s +- job: + name: charm-build-magnum-k8s + description: Build sunbeam magnum-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/magnum-k8s/* + vars: + charm: magnum-k8s +- job: + name: charm-build-designate-k8s + description: Build sunbeam designate-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-k8s/* + vars: + charm: designate-k8s +- job: + name: charm-build-designate-bind-k8s + description: Build sunbeam designate-bind-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-bind-k8s/* + vars: + charm: designate-bind-k8s +- job: + name: charm-build-keystone-ldap-k8s + description: Build sunbeam keystone-ldap-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-ldap-k8s/* + vars: + charm: keystone-ldap-k8s +- job: + name: charm-build-openstack-exporter-k8s + description: Build sunbeam openstack-exporter-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/openstack-exporter-k8s/* + vars: + charm: openstack-exporter-k8s +- job: + name: charm-build-openstack-hypervisor + description: Build sunbeam openstack-hypervisor charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/openstack-hypervisor/* + vars: + charm: openstack-hypervisor + +- job: + name: func-test-core + description: | + Zaza smoke test for all the core sunbeam charms. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-glance-k8s + soft: true + - name: charm-build-nova-k8s + soft: true + - name: charm-build-placement-k8s + soft: true + - name: charm-build-neutron-k8s + soft: true + - name: charm-build-ovn-central-k8s + soft: true + - name: charm-build-ovn-relay-k8s + soft: true + - name: charm-build-horizon-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-k8s/* + - charms/glance-k8s/* + - charms/nova-k8s/* + - charms/neutron-k8s/* + - charms/placement-k8s/* + - charms/ovn-central-k8s/* + - charms/ovn-relay-k8s/* + - charms/horizon-k8s/* + vars: + # Artifacts will be downloaded from below charm jobs + charm_jobs: + - charm-build-keystone-k8s + - charm-build-glance-k8s + - charm-build-nova-k8s + - charm-build-placement-k8s + - charm-build-neutron-k8s + - charm-build-ovn-central-k8s + - charm-build-ovn-relay-k8s + - charm-build-horizon-k8s + # test_dir relative to project src dir + test_dir: tests/core +- job: + name: func-test-ceph + description: | + Zaza smoke test for all the sunbeam charms that + requires storage/ceph. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-cinder-k8s + soft: true + - name: charm-build-cinder-ceph-k8s + soft: true + - name: charm-build-gnocchi-k8s + soft: true + - name: charm-build-ceilometer-k8s + soft: true + - name: charm-build-aodh-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-k8s/* + - charms/cinder-ceph-k8s/* + - charms/gnocchi-k8s/* + - charms/ceilometer-k8s/* + - charms/aodh-k8s/* + vars: + charm_jobs: + - charm-build-cinder-k8s + - charm-build-cinder-ceph-k8s + - charm-build-gnocchi-k8s + - charm-build-ceilometer-k8s + - charm-build-aodh-k8s + - charm-build-keystone-k8s + test_dir: tests/ceph +- job: + name: func-test-caas + description: | + Zaza smoke test for magnum and dependent charms + like heat, octavia, barbican. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-heat-k8s + soft: true + - name: charm-build-octavia-k8s + soft: true + - name: charm-build-barbican-k8s + soft: true + - name: charm-build-magnum-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-glance-k8s + soft: true + - name: charm-build-ovn-central-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/heat-k8s/* + - charms/octavia-k8s/* + - charms/barbican-k8s/* + - charms/magnum-k8s/* + vars: + charm_jobs: + - charm-build-heat-k8s + - charm-build-octavia-k8s + - charm-build-barbican-k8s + - charm-build-magnum-k8s + - charm-build-keystone-k8s + - charm-build-glance-k8s + - charm-build-ovn-central-k8s + test_dir: tests/caas +- job: + name: func-test-misc + description: | + Zaza smoke test for designate, desginate-bind, + keystone-ldap, openstack-exporter charms. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-designate-k8s + soft: true + - name: charm-build-designate-bind-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-keystone-ldap-k8s + soft: true + - name: charm-build-openstack-exporter-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-k8s/* + - charms/designate-bind-k8s/* + - charms/keystone-ldap-k8s/* + - charms/openstack-exporter-k8s/* + vars: + charm_jobs: + - charm-build-designate-k8s + - charm-build-designate-bind-k8s + - charm-build-keystone-ldap-k8s + - charm-build-openstack-exporter-k8s + - charm-build-keystone-k8s + test_dir: tests/misc + +- job: + name: publish-charms + description: | + Publish all the charms built in the gate + pipeline. + post-review: true + run: playbooks/charm/publish.yaml + secrets: + charmhub_token + timeout: 3600 diff --git a/zuul.d/project-templates.yaml b/zuul.d/project-templates.yaml new file mode 100644 index 00000000..5c22e69e --- /dev/null +++ b/zuul.d/project-templates.yaml @@ -0,0 +1,137 @@ +- project-template: + name: openstack-python3-sunbeam-jobs + # NOTE(hemanth): This template is used in openstack sunbeam charms since + # stable/2023.1. The stable/2023.1 and stable/2023.2 charm branches + # support py310 unit tests and main support py310, py311 tests. + description: | + Runs unit tests for an OpenStack Sunbeam project under the CPython + version 3 releases designated for testing the latest release. + check: + jobs: + - openstack-tox-pep8 + - openstack-tox-py310: + branches: + - stable/2023.1 + - stable/2023.2 + - main + - openstack-tox-py311: + branches: + - main + gate: + jobs: + - openstack-tox-pep8 + - openstack-tox-py310: + branches: + - stable/2023.1 + - stable/2023.2 + - main + - openstack-tox-py311: + branches: + - main + +- project-template: + name: openstack-sunbeam-charm-build-jobs + description: | + Build the charms in OpenStack Sunbeam project. + check: + fail-fast: true + jobs: + - charm-build-keystone-k8s: + nodeset: ubuntu-focal + - charm-build-glance-k8s: + nodeset: ubuntu-focal + - charm-build-nova-k8s: + nodeset: ubuntu-focal + - charm-build-placement-k8s: + nodeset: ubuntu-focal + - charm-build-neutron-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-central-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-relay-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-ceph-k8s: + nodeset: ubuntu-focal + - charm-build-horizon-k8s: + nodeset: ubuntu-focal + - charm-build-heat-k8s: + nodeset: ubuntu-focal + - charm-build-octavia-k8s: + nodeset: ubuntu-focal + - charm-build-aodh-k8s: + nodeset: ubuntu-focal + - charm-build-ceilometer-k8s: + nodeset: ubuntu-focal + - charm-build-gnocchi-k8s: + nodeset: ubuntu-focal + - charm-build-barbican-k8s: + nodeset: ubuntu-focal + - charm-build-magnum-k8s: + nodeset: ubuntu-focal + - charm-build-designate-k8s: + nodeset: ubuntu-focal + - charm-build-designate-bind-k8s: + nodeset: ubuntu-focal + - charm-build-keystone-ldap-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-exporter-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-hypervisor: + nodeset: ubuntu-focal + gate: + fail-fast: true + jobs: + - charm-build-keystone-k8s: + nodeset: ubuntu-focal + - charm-build-glance-k8s: + nodeset: ubuntu-focal + - charm-build-nova-k8s: + nodeset: ubuntu-focal + - charm-build-placement-k8s: + nodeset: ubuntu-focal + - charm-build-neutron-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-central-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-relay-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-ceph-k8s: + nodeset: ubuntu-focal + - charm-build-horizon-k8s: + nodeset: ubuntu-focal + - charm-build-heat-k8s: + nodeset: ubuntu-focal + - charm-build-octavia-k8s: + nodeset: ubuntu-focal + - charm-build-aodh-k8s: + nodeset: ubuntu-focal + - charm-build-ceilometer-k8s: + nodeset: ubuntu-focal + - charm-build-gnocchi-k8s: + nodeset: ubuntu-focal + - charm-build-barbican-k8s: + nodeset: ubuntu-focal + - charm-build-magnum-k8s: + nodeset: ubuntu-focal + - charm-build-designate-k8s: + nodeset: ubuntu-focal + - charm-build-designate-bind-k8s: + nodeset: ubuntu-focal + - charm-build-keystone-ldap-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-exporter-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-hypervisor: + nodeset: ubuntu-focal + +- project-template: + name: charm-publish-jobs + description: | + The set of publish jobs for the OpenStack Sunbeam Charms + promote: + jobs: + - publish-charms diff --git a/zuul.d/secrets.yaml b/zuul.d/secrets.yaml new file mode 100644 index 00000000..45abce9d --- /dev/null +++ b/zuul.d/secrets.yaml @@ -0,0 +1,65 @@ +- secret: + name: charmhub_token + data: + # Generated on 27-Nov-2023 with 90 days ttl + value: !encrypted/pkcs1-oaep + - oWjNK48hexdzzazFW8SA6M3otdZtNYV3cYABxa+q0NPog347A4jzdH0vhwI/wO8X0H6l0 + ohgH54pI7jjfFGuG46LTZWgWyw7YET/jI7r025Oz9yiDl5178Zk0foPrTWjRnF0rQdKA3 + V90Tmfn8xhHabrn2T5CFUrhvNnUQHxRMKy77jA/1hjmexC8E5NA2ypBIpPyOhwrOcyLZ8 + t6PsgiSRiSxr7d8eWZBeBAo3LU3KFTBGCsBWp/kzVQCzItRN7SbCf5i8JXHPUySnyRCYT + UzMeb2FtlAf98Ng4SyGXys1iQ4zKn75PDgWp4FeJyociDCDlnuAlGmh3GQ+MkXWAPl+vU + RYtqnvkvhNl2p3rRTzUUlrrttTpWhj4Cp41whOgoe6ZLPaRjboXizQClp8qbGGIdgb980 + aiTcFegNZl2RRersfgSwIqq+ys+SgPkQ1kFXpFDuLeCqL/OTEP3fBYAE4h3iIsmo0LSWH + /I9Oe8LQr1TQUEAlUNKMNIhlC8nL5uUOQXxHAmmIVQHNZYF1tObehMnFCWvFEWh3C5CGD + QgrT5zvTHUoH3GOfhDTEMH2ssft9rRJgcUieNlGvfMiypoJvQL51vfTEOHgUqBfqF6FCs + 6FP/eewHUFxx00HyQmRSspRbnq6u7iTcWsZFOrY/Szo62EhEOHgKqMedpEGbRo= + - YIPNXBTQ65iqKpDbI8fKSG4gwYi58ORqCIrHPOiyGjuQiHbwFeExzZYtuFxPB7EYRJPwJ + 0rrkBF434z7ySjovdyBwtB2svHLqcreB5RYIc4hWXzQN1QRUuSi+j79IQ27EAmkryY3OY + dR504mVOIBkLBKgS3ETiWvVLiOcHCiZD6mnq/MnLFg1xDrWb1aGQujBjeUDMDK1I7Qay8 + wY+3MGnrsmqZjmrdzN7loUp/TxR9rtXWFKmIXLyKGeadSOTIIh/6nVQJfM72NUWcYpDUh + 22EonzEZ0l9f+rWNeGx5ww8X2BRffSfeOPaJZSp3gMfljuA5o0HEAlTKTR0uA1iKp7iVR + BAW91yZPdIGk2oYA9ytfjx7LZCK6Xxax2dxtaHoqyBm9jeyrJ1p6IbiqRDp7lP3IsuCbe + g1iXItY6IhF8kthUFUTxQP/yf94KmQR8tY8cpEhXPhCEaQLitW67fIlkc9grGwIg644IY + Rqd5TCxB2FQ9N71aK9smcTMfQZbNXqH4n9Ix3KcAaknnKEuWCR5+A7bKXG2VIsofl3cc5 + QmvyBzACoSvIeJHB73YABlesCKxBuBZX2lQQv3GB/KFilxtRAw6oPpZY744F9k1MFNO/R + NuZQJ3yVCq3QkWpM66kE774zR+3ZlQ639zTWZOXeZsjzZHuBSzRhnlezCk35mA= + - N7LtSHy4KoQiGMTcdZGDdawEB2BdbDQZabzQu0NnkW5kYMQ/hFqoRwPjLTB1mC0f/uJYy + jIgdQNX8uIXn6eAAuyaID9CJb9QO6rQ6+ZAHtj5FIpiCIoykEW4XpIZrIGFYWlgCjn87f + UDQgX3s5JUU5PAGUzaw2EWL3RtHgmHX6/Syh+tTS77qA1jam9edf5TwQ1JbXMERsdnl+k + fjs+THAzU7kt3XuMm08juZBkEvKP8lS87hmmYJWWDdpyxhph6FIhJ3LTOhJmQmN9ThUOL + kDOh5SueP9FOXF+2ozrJkq+PpcJQswZLiftKS54WSeFsIASeneGH1ZpVaMUKrE0XfFo5e + RVkUZC2MWJw0K4rDZmtBD0DD3LXfN22l3Pq0CJJb2p0pFbJ/x8+Xp7KCmuPYt559oeyko + H0ZMsxYlVgVwPB+6mkhck7UYZlGKpMZlvE7PTRCMHQ2bLT+GMGdx6vcHYJP/DQDvsf/H+ + z1j0K5HfjlU6RM4GNrbT2xAxyCDsdFfsB0sevjLr3HAsTCzBw2AYf9VoNGlGCVFqt2Sru + CQD138fDI27KcwO+rVPoJsmrZEGMDhUZDhZVySZ80xTojDOMHF3RcYDcfnOOt22kQiZJD + 8bNIw3E1++l+tv9d3Ki14Bpwlkm2SPw4mMPGHb4xxo15TYu/MPIp0CD18K1jCM= + - FtedxbTxdhPCJSwkx94pTapAwR1wbSABS/RMsluoNzxL74sv+xyNCkHVhSSTijPCEs5GH + +xPK4TVV2h065j8B2AlYmj2QspZc2F0BmhVLFQXU1k0yeGvnE2aID1WXrXteh9m6dW8i7 + NHVcnuFuvZDL0/zAbMhn2kQ0SycyQ11z/ZPil2Q3RA9EMSCjI16QSJ2HrVg6UDiKmBTSI + c+J8mrT2Fw1ChlHQvea/x0BH/piFoFWGJYOLo63v4eCmpfvaZbkzOUqjgE6RA4w40FtTv + UAnIM6NidNoFnneOCYRxmem8PBYO6HbeCh11NoS69cfT47Jb7u/JoEL4cCqAdFZDUmVnd + zelgu4RG/2G2fQ90+Co3f7wJ10INlw3R22SUMf4CpotSyxiEpWDEmwn1M1GjjKKJiSq/X + WCY0ZHxkipcMdr6g0rgZ9a6Ousc10W+/5IKKLxgEvqdRX32U8tSvvOXC6zGGbxTjvSsbL + RvjbOYXt93caowz1uXtKrerqC545f6yiS9rDCOwMN0KQemRvP8J42Rdik/MgUsWuGhlZs + neCGIWHdNLfLSAInSHrYLw5KiSmBAfHuVjvVJsLXsw7fNzkl3PorxjJX3ypOe8xoT7o+V + xdoqdxgLxpR81OEs4Zsqx4tkSuVijUlPk3UUl224uqKsAOVjLKdbLLF0PIV4u8= + - iAneLfeh/CYMYf1ilfpDERn5KdPEgjYcQZRsFV1iw58Gvn8nQgXj7YB0iHZIT32Jn2JnO + +duKUh0aqHcrlf44cHBqQkq5EkbyR4prA0fHoO7Y8e16gAhkfmWhm2yr48XD0HAuSk6ID + PTvOw60T+aS3yafJzI7H91HqVVol1kM9zwwLCDIsx6OG5EFh6P98WYLNZH4isyWEXvgAj + ot3AR6kO//bXQLy1k+p4ECCVx3N9PtdoodehX2m9PUwx/VkXfNiJx53arsTPZe3mTcDCC + EZC/Lp73j2F68uQgwSyKQeQnrxaq0OwGbxzetarjheZLPH8ilB4AthnP29rn+y3Ydp37V + WVhV8O4zm+nbl7bsKF7BtadJy3TxR865wl8zLI7XtA+K7SZ4SpEHH0qiy8zRSlBLTTil6 + E0li2xMPkyO8QC4I2Sbvj7LqfpfbK40UVXdte/nu0hpOw5rBxh4wp8l8jGUq8XYliJqkv + g4dvFZXeHTTm9IKft5LYZUUUIW2FZyg6Ds1qHkA0WFBNK2hymabgmN27R7srk2y0A5UV9 + 17VAo1zx7TniLlc3P163eLBR4xaNNOVyVcmSF7UJWKwg/Al6mRLuteDJrZUlWfZ+EmMe0 + m9WG9y3RElsqvFabkIoUr0s95eZHv6IG+wo/4wpn7wzFJl05rsxlpeT/y8X/AI= + - TFcU1ReK6vwjV3JBQa3mufB8GObXLreHIf7Ziu+rZ1rAe9O1eymPjtS1YiCmny8Jo4zRy + EN/BLuWljfGrQKRwfP3FJa/oOcRq8m9l6uGNsu/qs4aSEPoqWhSum6FJKII/1WDpY1619 + ckbUsFxyXWYbgS0Z65S5XGFHi4zUkAm2F8askpE3fmZ18kiTxw8evh99nfQWhblXAqEuO + yIyR+FpAXnHnnBBXpOI1Q+h+rfPQ9wACOo8LQKkEhZMWVh0mgNjlOifOS4ufnUlp+b87P + whDyahW2ZbWt2E9Zt+S3PheL6EQYO4WOjM/IyExj2dlViv2oTMs+NhK29I4e7e/danDon + +HY5yEOhvl3ecJNXxQQlW2QZE9RQfJQ5xAGQkUMIdwlJqeC0E8KAtYmVylE+D3k7gzQaY + /vvlqAA7YzRmqF5s6DkTN/FyOnd2IIVCxJvkCsudWzvegX9FUpIUg1UFhHtEKfd1N/ab6 + DmlpWzg7SivW4Vk07BN9nR/ONa0+B7c5VmQ0POH2VZaIVbalG18DKkoVZqMxDRB+MS1Hf + XUzhH6kz9zTyUcDfXY76VDQFiriL2IzlDyCWk+xvXSuEIu8EEfilNr0l6GgBppvKogb2y + HxZtKfLQNQrBszndmq6QLc0+6nPa8M6egt7RbeRgxyrWqqM6m9MfPVDKjOmw78= diff --git a/zuul.d/zuul.yaml b/zuul.d/zuul.yaml new file mode 100644 index 00000000..4eabeb1c --- /dev/null +++ b/zuul.d/zuul.yaml @@ -0,0 +1,48 @@ +- project: + templates: + - openstack-python3-sunbeam-jobs + - openstack-cover-jobs + - openstack-sunbeam-charm-build-jobs + check: + jobs: + - func-test-core: + nodeset: ubuntu-focal + voting: false + - func-test-ceph: + nodeset: ubuntu-focal + voting: false + - func-test-caas: + nodeset: ubuntu-focal + voting: false + - func-test-misc: + nodeset: ubuntu-focal + voting: false + vars: + juju_channel: 3.2/stable + juju_classic_mode: false + microk8s_channel: 1.28-strict/stable + microk8s_classic_mode: false + charmcraft_channel: 2.0/stable + publish_channels: + keystone-k8s: 2023.2/edge + glance-k8s: 2023.2/edge + nova-k8s: 2023.2/edge + placement-k8s: 2023.2/edge + neutron-k8s: 2023.2/edge + ovn-central-k8s: 23.09/edge + ovn-relay-k8s: 23.09/edge + cinder-k8s: 2023.2/edge + cinder-ceph-k8s: 2023.2/edge + horizon-k8s: 2023.2/edge + heat-k8s: 2023.2/edge + octavia-k8s: 2023.2/edge + aodh-k8s: 2023.2/edge + ceilometer-k8s: 2023.2/edge + gnocchi-k8s: 2023.2/edge + barbican-k8s: 2023.2/edge + designate-k8s: 2023.2/edge + designate-bind-k8s: 9/edge + magnum-k8s: 2023.2/edge + keystone-ldap-k8s: 2023.2/edge + openstack-exporter-k8s: 2023.2/edge + openstack-hypervisor: 2023.2/edge