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 new file mode 100644 index 00000000..016e1ba2 --- /dev/null +++ b/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py @@ -0,0 +1,224 @@ +"""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/metadata.yaml b/charms/openstack-hypervisor/metadata.yaml index eede17cf..1e7a8200 100644 --- a/charms/openstack-hypervisor/metadata.yaml +++ b/charms/openstack-hypervisor/metadata.yaml @@ -17,6 +17,9 @@ requires: certificates: interface: tls-certificates optional: true + ceilometer-service: + interface: ceilometer + optional: true provides: cos-agent: diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index 3c104564..7f3de212 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -34,6 +34,10 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +from charms.ceilometer_k8s.v0.ceilometer_service import ( + CeilometerConfigChangedEvent, + CeilometerServiceGoneAwayEvent, +) from charms.grafana_agent.v0.cos_agent import COSAgentProvider from ops.charm import ActionEvent from ops.main import main @@ -56,6 +60,8 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): super().__init__(framework) self._state.set_default(metadata_secret="") self.enable_monitoring = self.check_relation_exists("cos-agent") + # Enable telemetry when ceilometer-service relation is joined + self.enable_telemetry = self.check_relation_exists("ceilometer-service") self.framework.observe( self.on.set_hypervisor_local_settings_action, self._set_hypervisor_local_settings_action, @@ -104,6 +110,14 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): "ovsdb-cms" in self.mandatory_relations, ) handlers.append(self.ovsdb_cms) + if self.can_add_handler("ceilometer-service", handlers): + self.ceilometer = sunbeam_rhandlers.CeilometerServiceRequiresHandler( + self, + "ceilometer-service", + self.handle_ceilometer_events, + "ceilometer-service" in self.mandatory_relations, + ) + handlers.append(self.ceilometer) handlers = super().get_relation_handlers(handlers) return handlers @@ -238,10 +252,35 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): } except AttributeError as e: raise sunbeam_guard.WaitingExceptionError("Data missing: {}".format(e.name)) + + # Handle optional config contexts + try: + if contexts.ceilometer_service.telemetry_secret: + snap_data.update( + { + "telemetry.enable": self.enable_telemetry, + "telemetry.publisher-secret": contexts.ceilometer_service.telemetry_secret, + } + ) + else: + snap_data.update({"telemetry.enable": self.enable_telemetry}) + except AttributeError: + logger.debug("ceilometer_service relation not integrated") + snap_data.update({"telemetry.enable": self.enable_telemetry}) + self.set_snap_data(snap_data) self.ensure_services_running() self._state.unit_bootstrapped = True + def handle_ceilometer_events(self, event: ops.framework.EventBase) -> None: + """Handle ceilometer events.""" + if isinstance(event, CeilometerConfigChangedEvent): + self.enable_telemetry = True + self.configure_charm(event) + elif isinstance(event, CeilometerServiceGoneAwayEvent): + self.enable_telemetry = False + self.configure_charm(event) + if __name__ == "__main__": # pragma: no cover main(HypervisorOperatorCharm) diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index ab3fe435..7e75eb09 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -77,6 +77,61 @@ class TestCharm(test_utils.CharmTestCase): }, ) + def test_mandatory_relations(self): + """Test all the charms relations.""" + self.get_local_ip_by_default_route.return_value = "10.0.0.10" + hypervisor_snap_mock = mock.MagicMock() + hypervisor_snap_mock.present = False + self.snap.SnapState.Latest = "latest" + self.snap.SnapCache.return_value = {"openstack-hypervisor": hypervisor_snap_mock} + self.socket.getfqdn.return_value = "test.local" + self.initial_setup() + self.harness.set_leader() + hypervisor_snap_mock.ensure.assert_any_call("latest", channel="essex/stable") + test_utils.add_complete_amqp_relation(self.harness) + test_utils.add_complete_identity_credentials_relation(self.harness) + metadata = self.harness.charm.metadata_secret() + ovn_cacert = test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN) + ovn_cacert = base64.b64encode(ovn_cacert.encode()).decode() + private_key = base64.b64encode( + self.harness.charm.contexts().certificates.key.encode() + ).decode() + certificate = base64.b64encode(test_utils.TEST_SERVER_CERT.encode()).decode() + expect_settings = { + "compute.cpu-mode": "host-model", + "compute.spice-proxy-address": "10.0.0.10", + "compute.virt-type": "kvm", + "credentials.ovn-metadata-proxy-shared-secret": metadata, + "identity.admin-role": None, + "identity.auth-url": "http://10.153.2.45:80/openstack-keystone", + "identity.password": "user-password", + "identity.project-domain-id": "pdomain-id", + "identity.project-domain-name": "pdomain_-ame", + "identity.project-name": "user-project", + "identity.region-name": "region12", + "identity.user-domain-id": "udomain-id", + "identity.user-domain-name": "udomain-name", + "identity.username": "username", + "logging.debug": False, + "monitoring.enable": False, + "network.dns-domain": "openstack.local", + "network.dns-servers": "8.8.8.8", + "network.enable-gateway": False, + "network.external-bridge": "br-ex", + "network.external-bridge-address": "10.20.20.1/24", + "network.ip-address": "10.0.0.10", + "network.ovn-cacert": ovn_cacert, + "network.ovn-cert": certificate, + "network.ovn-key": private_key, + "network.ovn-sb-connection": "ssl:10.20.21.10:6642", + "network.physnet-name": "physnet1", + "node.fqdn": "test.local", + "node.ip-address": "10.0.0.10", + "rabbitmq.url": "rabbit://hypervisor:rabbit.pass@10.0.0.13:5672/openstack", + "telemetry.enable": False, + } + hypervisor_snap_mock.set.assert_any_call(expect_settings, typed=True) + def test_all_relations(self): """Test all the charms relations.""" # Add cos-agent relation @@ -91,6 +146,11 @@ class TestCharm(test_utils.CharmTestCase): }, ) + # Add ceilometer-service relation + self.harness.add_relation( + "ceilometer-service", "ceilometer", app_data={"telemetry-secret": "FAKE_SECRET"} + ) + self.get_local_ip_by_default_route.return_value = "10.0.0.10" hypervisor_snap_mock = mock.MagicMock() hypervisor_snap_mock.present = False @@ -125,6 +185,7 @@ class TestCharm(test_utils.CharmTestCase): "identity.user-domain-name": "udomain-name", "identity.username": "username", "logging.debug": False, + "monitoring.enable": True, "network.dns-domain": "openstack.local", "network.dns-servers": "8.8.8.8", "network.enable-gateway": False, @@ -139,6 +200,7 @@ class TestCharm(test_utils.CharmTestCase): "node.fqdn": "test.local", "node.ip-address": "10.0.0.10", "rabbitmq.url": "rabbit://hypervisor:rabbit.pass@10.0.0.13:5672/openstack", - "monitoring.enable": True, + "telemetry.enable": True, + "telemetry.publisher-secret": "FAKE_SECRET", } hypervisor_snap_mock.set.assert_any_call(expect_settings, typed=True)