From 0bdc19c4ea477180d0bab4ec43caee0ef96f596e Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Thu, 10 Oct 2024 07:34:37 +0530 Subject: [PATCH] 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 --- charms/masakari-k8s/.sunbeam-build.yaml | 1 + charms/masakari-k8s/charmcraft.yaml | 4 + charms/masakari-k8s/src/charm.py | 33 +++ .../openstack-hypervisor/.sunbeam-build.yaml | 1 + charms/openstack-hypervisor/charmcraft.yaml | 2 + charms/openstack-hypervisor/src/charm.py | 20 ++ .../tests/unit/test_charm.py | 9 + charms/sunbeam-libs/.sunbeam-build.yaml | 3 + charms/sunbeam-libs/charmcraft.yaml | 25 +++ .../lib/charms/sunbeam_libs/v0/py.typed | 0 .../sunbeam_libs/v0/service_readiness.py | 203 ++++++++++++++++++ charms/sunbeam-libs/src/charm.py | 36 ++++ charms/sunbeam-libs/tests/unit/__init__.py | 17 ++ charms/sunbeam-libs/tests/unit/test_charm.py | 59 +++++ ops-sunbeam/ops_sunbeam/relation_handlers.py | 135 ++++++++++++ 15 files changed, 548 insertions(+) create mode 100644 charms/sunbeam-libs/.sunbeam-build.yaml create mode 100644 charms/sunbeam-libs/charmcraft.yaml create mode 100644 charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/py.typed create mode 100644 charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/service_readiness.py create mode 100755 charms/sunbeam-libs/src/charm.py create mode 100644 charms/sunbeam-libs/tests/unit/__init__.py create mode 100644 charms/sunbeam-libs/tests/unit/test_charm.py diff --git a/charms/masakari-k8s/.sunbeam-build.yaml b/charms/masakari-k8s/.sunbeam-build.yaml index a6db8feb..ed7db18d 100644 --- a/charms/masakari-k8s/.sunbeam-build.yaml +++ b/charms/masakari-k8s/.sunbeam-build.yaml @@ -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 diff --git a/charms/masakari-k8s/charmcraft.yaml b/charms/masakari-k8s/charmcraft.yaml index c6507941..5c5be596 100644 --- a/charms/masakari-k8s/charmcraft.yaml +++ b/charms/masakari-k8s/charmcraft.yaml @@ -98,6 +98,10 @@ requires: limit: 1 optional: true +provides: + masakari-service: + interface: service-ready + peers: peers: interface: masakari-peer diff --git a/charms/masakari-k8s/src/charm.py b/charms/masakari-k8s/src/charm.py index 5f354e7e..dc8e293d 100755 --- a/charms/masakari-k8s/src/charm.py +++ b/charms/masakari-k8s/src/charm.py @@ -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.""" diff --git a/charms/openstack-hypervisor/.sunbeam-build.yaml b/charms/openstack-hypervisor/.sunbeam-build.yaml index 294bb11d..5d84fb07 100644 --- a/charms/openstack-hypervisor/.sunbeam-build.yaml +++ b/charms/openstack-hypervisor/.sunbeam-build.yaml @@ -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 diff --git a/charms/openstack-hypervisor/charmcraft.yaml b/charms/openstack-hypervisor/charmcraft.yaml index c9aea95a..2b420416 100644 --- a/charms/openstack-hypervisor/charmcraft.yaml +++ b/charms/openstack-hypervisor/charmcraft.yaml @@ -80,6 +80,8 @@ requires: optional: true nova-service: interface: nova + masakari-service: + interface: service-ready tracing: interface: tracing optional: true diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index 50999a07..77f7545a 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -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: diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index 7d2e4631..3ea5efee 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -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) diff --git a/charms/sunbeam-libs/.sunbeam-build.yaml b/charms/sunbeam-libs/.sunbeam-build.yaml new file mode 100644 index 00000000..7dc2f566 --- /dev/null +++ b/charms/sunbeam-libs/.sunbeam-build.yaml @@ -0,0 +1,3 @@ +external-libraries: [] +internal-libraries: [] +templates: [] diff --git a/charms/sunbeam-libs/charmcraft.yaml b/charms/sunbeam-libs/charmcraft.yaml new file mode 100644 index 00000000..4e7b21fa --- /dev/null +++ b/charms/sunbeam-libs/charmcraft.yaml @@ -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 diff --git a/charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/py.typed b/charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/service_readiness.py b/charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/service_readiness.py new file mode 100644 index 00000000..6254a35e --- /dev/null +++ b/charms/sunbeam-libs/lib/charms/sunbeam_libs/v0/service_readiness.py @@ -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 diff --git a/charms/sunbeam-libs/src/charm.py b/charms/sunbeam-libs/src/charm.py new file mode 100755 index 00000000..58ec09d8 --- /dev/null +++ b/charms/sunbeam-libs/src/charm.py @@ -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) diff --git a/charms/sunbeam-libs/tests/unit/__init__.py b/charms/sunbeam-libs/tests/unit/__init__.py new file mode 100644 index 00000000..a23c5174 --- /dev/null +++ b/charms/sunbeam-libs/tests/unit/__init__.py @@ -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.""" diff --git a/charms/sunbeam-libs/tests/unit/test_charm.py b/charms/sunbeam-libs/tests/unit/test_charm.py new file mode 100644 index 00000000..e6b0d8d7 --- /dev/null +++ b/charms/sunbeam-libs/tests/unit/test_charm.py @@ -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"], + ) diff --git a/ops-sunbeam/ops_sunbeam/relation_handlers.py b/ops-sunbeam/ops_sunbeam/relation_handlers.py index aae02381..35da32d2 100644 --- a/ops-sunbeam/ops_sunbeam/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/relation_handlers.py @@ -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