diff --git a/charms/ceilometer-k8s/README.md b/charms/ceilometer-k8s/README.md index f48345bf..336f917f 100644 --- a/charms/ceilometer-k8s/README.md +++ b/charms/ceilometer-k8s/README.md @@ -14,11 +14,12 @@ ceilometer-k8s is deployed using below command: juju deploy ceilometer-k8s ceilometer --trust -Now connect the ceilometer operator to keystone identity and rabbitmq -operators: +Now connect the ceilometer operator to keystone identity, rabbitmq +and gnocchi operators: juju relate keystone:identity-service ceilometer:identity-service juju relate rabbitmq:amqp ceilometer:amqp + juju relate gnocchi:gnocchi-service ceilometer:gnocchi-db ### Configuration @@ -40,12 +41,13 @@ ceilometer-k8s requires the following relations: `identity-service`: To register endpoints in Keystone `amqp`: To connect to Rabbitmq +`gnocchi-db`: To connect to Gnocchi database ## OCI Images The charm by default uses following images: - ghcr.io/canonical/ceilometer-consolidated:2023.1 + ghcr.io/canonical/ceilometer-consolidated:2023.2 ## Contributing diff --git a/charms/ceilometer-k8s/actions.yaml b/charms/ceilometer-k8s/actions.yaml index a19df938..88e6195d 100644 --- a/charms/ceilometer-k8s/actions.yaml +++ b/charms/ceilometer-k8s/actions.yaml @@ -1,7 +1,2 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -ceilometer-upgrade: - description: | - Perform ceilometer-upgrade. This action will update Ceilometer data store - configuration. +# NOTE: no actions yet! +{ } diff --git a/charms/ceilometer-k8s/fetch-libs.sh b/charms/ceilometer-k8s/fetch-libs.sh index f7433018..16cf2cad 100755 --- a/charms/ceilometer-k8s/fetch-libs.sh +++ b/charms/ceilometer-k8s/fetch-libs.sh @@ -1,6 +1,7 @@ #!/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 diff --git a/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py new file mode 100644 index 00000000..fbc679d5 --- /dev/null +++ b/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py @@ -0,0 +1,205 @@ +"""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/ceilometer-k8s/metadata.yaml b/charms/ceilometer-k8s/metadata.yaml index fcba7858..53339441 100644 --- a/charms/ceilometer-k8s/metadata.yaml +++ b/charms/ceilometer-k8s/metadata.yaml @@ -47,6 +47,8 @@ requires: identity-credentials: interface: keystone-credentials limit: 1 + gnocchi-db: + interface: gnocchi peers: peers: diff --git a/charms/ceilometer-k8s/src/charm.py b/charms/ceilometer-k8s/src/charm.py index f46e30d9..3499d37f 100755 --- a/charms/ceilometer-k8s/src/charm.py +++ b/charms/ceilometer-k8s/src/charm.py @@ -25,6 +25,7 @@ from typing import ( List, ) +import ops.charm import ops.framework import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers @@ -34,13 +35,19 @@ from charms.ceilometer_k8s.v0.ceilometer_service import ( CeilometerConfigRequestEvent, CeilometerServiceProvides, ) +from charms.gnocchi_k8s.v0.gnocchi_service import ( + GnocchiServiceRequires, +) from ops.charm import ( - ActionEvent, CharmBase, + RelationEvent, ) from ops.main import ( main, ) +from ops.model import ( + BlockedStatus, +) logger = logging.getLogger(__name__) @@ -48,6 +55,71 @@ CEILOMETER_CENTRAL_CONTAINER = "ceilometer-central" CEILOMETER_NOTIFICATION_CONTAINER = "ceilometer-notification" +class GnocchiServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): + """Handle gnocchi service relation on the requires side.""" + + def __init__( + self, + charm: CharmBase, + relation_name: str, + callback_f: Callable, + mandatory: bool = False, + ): + """Create a new gnocchi service handler. + + Create a new GnocchiServiceRequiresHandler that handles initial + events from the relation and invokes the provided callbacks based on + the event raised. + + :param charm: the Charm class the handler is for + :type charm: ops.charm.CharmBase + :param relation_name: the relation the handler is bound to + :type relation_name: str + :param callback_f: the function to call when the nodes are connected + :type callback_f: Callable + :param mandatory: If the relation is mandatory to proceed with + configuring charm + :type mandatory: bool + """ + super().__init__(charm, relation_name, callback_f, mandatory) + + def setup_event_handler(self) -> ops.charm.Object: + """Configure event handlers for Gnocchi service relation.""" + logger.debug("Setting up Gnocchi service event handler") + svc = GnocchiServiceRequires( + self.charm, + self.relation_name, + ) + self.framework.observe( + svc.on.readiness_changed, + self._on_gnocchi_service_readiness_changed, + ) + self.framework.observe( + svc.on.goneaway, + self._on_gnocchi_service_goneaway, + ) + return svc + + def _on_gnocchi_service_readiness_changed( + self, event: RelationEvent + ) -> None: + """Handle config_changed event.""" + logger.debug("Gnocchi service readiness changed event received") + self.callback_f(event) + + def _on_gnocchi_service_goneaway(self, event: RelationEvent) -> None: + """Handle gone_away event.""" + logger.debug("Gnocchi service gone away event received") + self.callback_f(event) + if self.mandatory: + self.status.set(BlockedStatus("integration missing")) + + @property + def ready(self) -> bool: + """Whether handler is ready for use.""" + return self.interface.service_ready + + class CeilometerServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for ceilometer service relation.""" @@ -177,13 +249,11 @@ class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): service_name = "ceilometer" shared_metering_secret_key = "shared-metering-secret" - mandatory_relations = {"amqp", "identity-credentials"} + db_sync_cmds = [["ceilometer-upgrade"]] + mandatory_relations = {"amqp", "identity-credentials", "gnocchi-db"} def __init__(self, framework: ops.framework): super().__init__(framework) - self.framework.observe( - self.on.ceilometer_upgrade_action, self._ceilometer_upgrade_action - ) def get_shared_meteringsecret(self): """Return the shared metering secret.""" @@ -210,6 +280,11 @@ class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): return super().configure_charm(event) + @property + def db_sync_container_name(self) -> str: + """Name of Containerto run db sync from.""" + return CEILOMETER_NOTIFICATION_CONTAINER + @property def service_user(self) -> str: """Service user file and directory ownership.""" @@ -266,6 +341,14 @@ class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): self.set_config_from_event, ) handlers.append(self.config_svc) + if self.can_add_handler("gnocchi-db", handlers): + self.gnocchi_svc = GnocchiServiceRequiresHandler( + self, + "gnocchi-db", + self.configure_charm, + "gnocchi-db" in self.mandatory_relations, + ) + handlers.append(self.gnocchi_svc) return super().get_relation_handlers(handlers) @@ -289,30 +372,6 @@ class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): else: logging.debug("Telemetry secret not yet set, not sending config") - def _ceilometer_upgrade_action(self, event: ActionEvent) -> None: - """Run ceilometer-upgrade. - - This action will upgrade the data store configuration in gnocchi. - """ - try: - logger.info("Syncing database...") - cmd = ["ceilometer-upgrade"] - container = self.unit.get_container( - CEILOMETER_NOTIFICATION_CONTAINER - ) - process = container.exec(cmd, timeout=5 * 60) - out, warnings = process.wait_output() - logging.debug("Output from database sync: \n%s", out) - if warnings: - for line in warnings.splitlines(): - logger.warning("DB Sync Out: %s", line.strip()) - event.fail(f"Error in running ceilometer-upgrade: {warnings}") - else: - event.set_results({"message": "ceilometer-upgrade successful"}) - except Exception as e: - logger.exception(e) - event.fail(f"Error in running ceilometer-updgrade: {e}") - if __name__ == "__main__": main(CeilometerOperatorCharm) diff --git a/charms/ceilometer-k8s/tests/bundles/smoke.yaml b/charms/ceilometer-k8s/tests/bundles/smoke.yaml index 300ce5b6..c70a12d8 100644 --- a/charms/ceilometer-k8s/tests/bundles/smoke.yaml +++ b/charms/ceilometer-k8s/tests/bundles/smoke.yaml @@ -93,3 +93,5 @@ relations: - ceilometer:amqp - - keystone:identity-credentials - ceilometer:identity-credentials +- - gnocchi:gnocchi-service + - ceilometer:gnocchi-db diff --git a/charms/ceilometer-k8s/tests/tests.yaml b/charms/ceilometer-k8s/tests/tests.yaml index c5587f17..e240b187 100644 --- a/charms/ceilometer-k8s/tests/tests.yaml +++ b/charms/ceilometer-k8s/tests/tests.yaml @@ -33,8 +33,8 @@ target_deploy_status: workload-status: active workload-status-message-regex: '^.*$' ceilometer: - workload-status: active - workload-status-message-regex: '^.*$' + workload-status: waiting + workload-status-message-regex: '^.*Not all relations are ready$' aodh: workload-status: active workload-status-message-regex: '^.*$' diff --git a/charms/ceilometer-k8s/tests/unit/test_charm.py b/charms/ceilometer-k8s/tests/unit/test_charm.py index cef35b38..ff3e7847 100644 --- a/charms/ceilometer-k8s/tests/unit/test_charm.py +++ b/charms/ceilometer-k8s/tests/unit/test_charm.py @@ -60,6 +60,9 @@ class TestCeilometerOperatorCharm(test_utils.CharmTestCase): test_utils.set_all_pebbles_ready(self.harness) test_utils.add_complete_identity_credentials_relation(self.harness) test_utils.add_complete_amqp_relation(self.harness) + self.harness.add_relation( + "gnocchi-db", "gnocchi", app_data={"ready": "true"} + ) for c in ["ceilometer-central", "ceilometer-notification"]: self.check_file(c, "/etc/ceilometer/ceilometer.conf")