Add support for masakarimonitors

* Add new interface service-ready to check for service
readiness of remote application.
* Create a placeholder charm sunbeam-libs to place all
the common libraries. The charm and the libraries need
not be published to charmhub since at this point of time
they are used internally by sunbeam.
* Add provider to service-ready in masakari-k8s
* Add requirer to service-ready in openstack-hypervisor
and enable/disable snap option masakari.enable based on
service-ready relation.

Change-Id: I99feccee2c871fc5a581fdea6f45a541efc2a968
This commit is contained in:
Hemanth Nakkina 2024-10-10 07:34:37 +05:30
parent bd137e4c39
commit 0bdc19c4ea
No known key found for this signature in database
GPG Key ID: 2E4970F7B143168E
15 changed files with 548 additions and 0 deletions

View File

@ -9,6 +9,7 @@ external-libraries:
- charms.tempo_k8s.v1.charm_tracing
internal-libraries:
- charms.keystone_k8s.v1.identity_service
- charms.sunbeam_libs.v0.service_readiness
templates:
- parts/database-connection
- parts/database-connection-settings

View File

@ -98,6 +98,10 @@ requires:
limit: 1
optional: true
provides:
masakari-service:
interface: service-ready
peers:
peers:
interface: masakari-peer

View File

@ -41,6 +41,9 @@ from charms.consul_k8s.v0.consul_cluster import (
from ops import (
main,
)
from ops.charm import (
RelationEvent,
)
from ops.model import (
BlockedStatus,
)
@ -362,6 +365,15 @@ class MasakariOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
)
handlers.append(self.consul_storage)
self.svc_ready_handler = (
sunbeam_rhandlers.ServiceReadinessProviderHandler(
self,
"masakari-service",
self.handle_readiness_request_from_event,
)
)
handlers.append(self.svc_ready_handler)
handlers = super().get_relation_handlers(handlers)
return handlers
@ -399,6 +411,27 @@ class MasakariOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
)
return pebble_handlers
def post_config_setup(self):
"""Configuration steps after services have been setup."""
super().post_config_setup()
self.set_readiness_on_related_units()
def handle_readiness_request_from_event(
self, event: RelationEvent
) -> None:
"""Set service readiness in relation data."""
self.svc_ready_handler.interface.set_service_status(
event.relation, self.bootstrapped()
)
def set_readiness_on_related_units(self) -> None:
"""Set service readiness on masakari-service related units."""
logger.debug(
"Set service readiness on all connected masakari-service relations"
)
for relation in self.framework.model.relations["masakari-service"]:
self.svc_ready_handler.interface.set_service_status(relation, True)
@property
def service_name(self):
"""Service name."""

View File

@ -15,3 +15,4 @@ internal-libraries:
- charms.cinder_ceph_k8s.v0.ceph_access
- charms.ceilometer_k8s.v0.ceilometer_service
- charms.nova_k8s.v0.nova_service
- charms.sunbeam_libs.v0.service_readiness

View File

@ -80,6 +80,8 @@ requires:
optional: true
nova-service:
interface: nova
masakari-service:
interface: service-ready
tracing:
interface: tracing
optional: true

View File

@ -292,6 +292,16 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
mandatory="certificates" in self.mandatory_relations,
)
handlers.append(self.certs)
if self.can_add_handler("masakari-service", handlers):
self.masakari_svc = (
sunbeam_rhandlers.ServiceReadinessRequiresHandler(
self,
"masakari-service",
self.configure_charm,
"masakari-service" in self.mandatory_relations,
)
)
handlers.append(self.masakari_svc)
handlers = super().get_relation_handlers(handlers)
return handlers
@ -468,6 +478,7 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
snap_data.update(self._handle_ceilometer_service(contexts))
snap_data.update(self._handle_nova_service(contexts))
snap_data.update(self._handle_receive_ca_cert(contexts))
snap_data.update(self._handle_masakari_service(contexts))
self.set_snap_data(snap_data)
self.ensure_services_running()
@ -518,6 +529,15 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
return {}
def _handle_masakari_service(
self, contexts: sunbeam_core.OPSCharmContexts
) -> dict:
try:
return {"masakari.enable": contexts.masakari_service.service_ready}
except AttributeError:
logger.info("masakari_service relation not integrated")
return {"masakari.enable": False}
def _handle_receive_ca_cert(
self, context: sunbeam_core.OPSCharmContexts
) -> dict:

View File

@ -162,6 +162,7 @@ class TestCharm(test_utils.CharmTestCase):
"rabbitmq.url": "rabbit://hypervisor:rabbit.pass@10.0.0.13:5672/openstack",
"telemetry.enable": False,
"ca.bundle": None,
"masakari.enable": False,
}
hypervisor_snap_mock.set.assert_any_call(expect_settings, typed=True)
@ -195,6 +196,13 @@ class TestCharm(test_utils.CharmTestCase):
},
)
# Add masakari-service relation
self.harness.add_relation(
"masakari-service",
"masakari",
app_data={"ready": "true"},
)
self.get_local_ip_by_default_route.return_value = "10.0.0.10"
hypervisor_snap_mock = MagicMock()
hypervisor_snap_mock.present = False
@ -266,5 +274,6 @@ class TestCharm(test_utils.CharmTestCase):
"telemetry.enable": True,
"telemetry.publisher-secret": "FAKE_SECRET",
"ca.bundle": None,
"masakari.enable": True,
}
hypervisor_snap_mock.set.assert_any_call(expect_settings, typed=True)

View File

@ -0,0 +1,3 @@
external-libraries: []
internal-libraries: []
templates: []

View File

@ -0,0 +1,25 @@
type: charm
title: Sunbeam common libraries
name: sunbeam-libs
summary: Sunbeam common libraries
description: |
Placeholder for the common libraries used in Sunbeam.
assumes:
- k8s-api
links:
source: https://opendev.org/openstack/sunbeam-charms
issues: https://bugs.launchpad.net/sunbeam-charms
base: ubuntu@24.04
platforms:
amd64:
containers:
placeholder:
resource: placeholder-image
resources:
placeholder-image:
description: OCI image for placeholder
type: oci-image
upstream-source: busybox

View File

@ -0,0 +1,203 @@
"""Service Provides and Requires module.
The interface `service-ready` is to inform that remote service is ready.
This library contains the Requires and Provides classes for handling
the service-ready interface.
Import `ServiceReadinessRequirer` in your charm, with the charm object and the
relation name:
- self
- "service"
Two events are also available to respond to:
- readiness_changed
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.masakari_k8s.v0.service_readiness import (
ServiceReadinessRequirer
)
class ServiceClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# Service Requires
self._svc = ServiceReadinessRequirer(
self, "service",
)
self.framework.observe(
self._svc.on.readiness_changed,
self._on_service_readiness_changed
)
self.framework.observe(
self._svc.on.goneaway,
self._on_service_goneaway
)
def _on_service_readiness_changed(self, event):
'''React to the service readiness changed event.
This event happens when service relation is added to the
model and relation data is changed.
'''
# Do something with the configuration provided by relation.
pass
def _on_service_goneaway(self, event):
'''React to the Service goneaway event.
This event happens when service relation is removed.
'''
# Service Relation has goneaway.
pass
```
"""
import json
import logging
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 = "706872aa869c11ef9444175192825660"
# 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 ServiceReadinessRequestEvent(RelationEvent):
"""ServiceReadinessRequest Event."""
pass
class ServiceReadinessProviderEvents(ObjectEvents):
"""Events class for `on`."""
service_readiness = EventSource(ServiceReadinessRequestEvent)
class ServiceReadinessProvider(Object):
"""ServiceReadinessProvider class."""
on = ServiceReadinessProviderEvents()
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 service relation changed."""
logging.debug(f"Service relation changed for relation {self.relation_name}")
self.on.service_readiness.emit(event.relation)
def set_service_status(self, relation: Relation, is_ready: bool) -> None:
"""Set 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 ServiceReadinessChangedEvent(RelationEvent):
"""ServiceReadinessChanged Event."""
pass
class ServiceGoneAwayEvent(RelationEvent):
"""ServiceGoneAway Event."""
pass
class ServiceReadinessRequirerEvents(ObjectEvents):
"""Events class for `on`."""
readiness_changed = EventSource(ServiceReadinessChangedEvent)
goneaway = EventSource(ServiceGoneAwayEvent)
class ServiceReadinessRequirer(Object):
"""ServiceReadinessRequirer class."""
on = ServiceReadinessRequirerEvents()
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 Service relation changed."""
logging.debug(f"service readiness data changed for relation {self.relation_name}")
self.on.readiness_changed.emit(event.relation)
def _on_relation_broken(self, event: RelationBrokenEvent):
"""Handle Service relation broken."""
logging.debug(f"service readiness relation broken for {self.relation_name}")
self.on.goneaway.emit(event.relation)
@property
def _service_rel(self) -> Relation | None:
"""The service relation."""
return self.framework.model.get_relation(self.relation_name)
def get_remote_app_data(self, key: str) -> str | None:
"""Return the value for the given key from remote app data."""
if self._service_rel:
data = self._service_rel.data[
self._service_rel.app
]
return data.get(key)
return None
@property
def service_ready(self) -> bool:
"""Return if service is ready or not."""
is_ready = self.get_remote_app_data("ready")
if is_ready:
return json.loads(is_ready)
return False

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
# Copyright 2024 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.
"""sunbeam-libs Charm.
This charm is a placeholder for sunbeam common libraries.
"""
import ops_sunbeam.charm as sunbeam_charm
from ops import (
main,
)
class SunbeamLibsCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"""Placeholder charm for Sunbeam common libs."""
@property
def service_name(self):
"""Service name."""
return "placeholder"
if __name__ == "__main__":
main(SunbeamLibsCharm)

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
# Copyright 2024 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.
"""Unit tests for sunbeam-libs."""

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
# Copyright 2024 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.
"""Tests for sunbeam-libs charm."""
import charm
import ops_sunbeam.test_utils as test_utils
class _SunbeamLibsCharm(charm.SunbeamLibsCharm):
"""Dummy class to satisfy reading proper charmcraft file."""
def __init__(self, framework):
self.seen_events = []
super().__init__(framework)
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
class TestSunbeamLibsCharm(test_utils.CharmTestCase):
"""Class for testing sunbeam-libs charm."""
def setUp(self):
"""Run setup for unit tests."""
super().setUp(charm, [])
self.harness = test_utils.get_harness(
_SunbeamLibsCharm,
container_calls=self.container_calls,
)
self.addCleanup(self.harness.cleanup)
def test_pebble_ready_handler(self):
"""Test Pebble ready event is captured."""
self.harness.begin()
self.assertEqual(self.harness.charm.seen_events, [])
test_utils.set_all_pebbles_ready(self.harness)
self.assertEqual(
self.harness.charm.seen_events,
["PebbleReadyEvent"],
)

View File

@ -57,6 +57,7 @@ if typing.TYPE_CHECKING:
import charms.loki_k8s.v1.loki_push_api as loki_push_api
import charms.nova_k8s.v0.nova_service as nova_service
import charms.rabbitmq_k8s.v0.rabbitmq as rabbitmq
import charms.sunbeam_libs.v0.service_readiness as service_readiness
import charms.tempo_k8s.v2.tracing as tracing
import charms.tls_certificates_interface.v3.tls_certificates as tls_certificates
import charms.traefik_k8s.v2.ingress as ingress
@ -2450,3 +2451,137 @@ class GnocchiServiceRequiresHandler(RelationHandler):
def ready(self) -> bool:
"""Whether handler is ready for use."""
return self.interface.service_ready
@sunbeam_tracing.trace_type
class ServiceReadinessRequiresHandler(RelationHandler):
"""Handle service-ready relation on the requires side."""
interface: "service_readiness.ServiceReadinessRequirer"
def __init__(
self,
charm: "OSBaseOperatorCharm",
relation_name: str,
callback_f: Callable,
mandatory: bool = False,
):
"""Create a new service-ready requirer handler.
Create a new ServiceReadinessRequiresHandler 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.framework.Object:
"""Configure event handlers for service-ready relation."""
import charms.sunbeam_libs.v0.service_readiness as service_readiness
logger.debug(
f"Setting up service-ready event handler for {self.relation_name}"
)
svc = sunbeam_tracing.trace_type(
service_readiness.ServiceReadinessRequirer
)(
self.charm,
self.relation_name,
)
self.framework.observe(
svc.on.readiness_changed,
self._on_remote_service_readiness_changed,
)
self.framework.observe(
svc.on.goneaway,
self._on_remote_service_goneaway,
)
return svc
def _on_remote_service_readiness_changed(
self, event: ops.framework.EventBase
) -> None:
"""Handle config_changed event."""
logger.debug(
f"Remote service readiness changed event received for relation {self.relation_name}"
)
self.callback_f(event)
def _on_remote_service_goneaway(
self, event: ops.framework.EventBase
) -> None:
"""Handle gone_away event."""
logger.debug(
"Remote service gone away event received for relation {self.relation_name}"
)
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
@sunbeam_tracing.trace_type
class ServiceReadinessProviderHandler(RelationHandler):
"""Handler for service-readiness relation on provider side."""
interface: "service_readiness.ServiceReadinessProvider"
def __init__(
self,
charm: "OSBaseOperatorCharm",
relation_name: str,
callback_f: Callable,
):
"""Create a new service-readiness provider handler.
Create a new ServiceReadinessProvidesHandler that updates service
readiness on the related units.
: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
"""
super().__init__(charm, relation_name, callback_f)
def setup_event_handler(self):
"""Configure event handlers for service-readiness relation."""
import charms.sunbeam_libs.v0.service_readiness as service_readiness
logger.debug(f"Setting up event handler for {self.relation_name}")
svc = sunbeam_tracing.trace_type(
service_readiness.ServiceReadinessProvider
)(
self.charm,
self.relation_name,
)
self.framework.observe(
svc.on.service_readiness,
self._on_service_readiness,
)
return svc
def _on_service_readiness(self, event: ops.framework.EventBase) -> None:
"""Handle service readiness request event."""
self.callback_f(event)
@property
def ready(self) -> bool:
"""Report if relation is ready."""
return True