diff --git a/charms/manila-cephfs-k8s/.sunbeam-build.yaml b/charms/manila-cephfs-k8s/.sunbeam-build.yaml index 4a420528..6947002c 100644 --- a/charms/manila-cephfs-k8s/.sunbeam-build.yaml +++ b/charms/manila-cephfs-k8s/.sunbeam-build.yaml @@ -7,6 +7,7 @@ external-libraries: - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_credentials + - charms.manila_k8s.v0.manila templates: - parts/section-database - parts/database-connection diff --git a/charms/manila-cephfs-k8s/README.md b/charms/manila-cephfs-k8s/README.md index 5d9b30a8..556a6753 100644 --- a/charms/manila-cephfs-k8s/README.md +++ b/charms/manila-cephfs-k8s/README.md @@ -24,6 +24,7 @@ keystone identity, and manila operators: juju relate rabbitmq:amqp manila-cephfs:amqp juju relate keystone:identity-credentials manila-cephfs:identity-credentials juju relate manila-cephfs:ceph-nfs admin/openstack-machines.microceph-ceph-nfs + juju relate manila:manila manila-cephfs:manila ### Configuration @@ -53,6 +54,10 @@ The following relations are optional: - `logging`: To send logs to Loki. - `tracing`: To connect to a tracing backend. +The charm provides the following relation: + +- `manila`: To provide Manila with the NFS storage backend. + ## OCI Images The charm by default uses follwoing images: diff --git a/charms/manila-cephfs-k8s/charmcraft.yaml b/charms/manila-cephfs-k8s/charmcraft.yaml index df069f14..c0394469 100644 --- a/charms/manila-cephfs-k8s/charmcraft.yaml +++ b/charms/manila-cephfs-k8s/charmcraft.yaml @@ -60,6 +60,10 @@ requires: optional: true limit: 1 +provides: + manila: + interface: manila-backend + parts: update-certificates: plugin: nil diff --git a/charms/manila-cephfs-k8s/src/charm.py b/charms/manila-cephfs-k8s/src/charm.py index fb00abf6..ec8145f9 100755 --- a/charms/manila-cephfs-k8s/src/charm.py +++ b/charms/manila-cephfs-k8s/src/charm.py @@ -27,6 +27,7 @@ from typing import ( ) import charms.ceph_nfs_client.v0.ceph_nfs_client as ceph_nfs_client +import charms.manila_k8s.v0.manila as manila_k8s import ops import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as sunbeam_ctxts @@ -39,6 +40,8 @@ logger = logging.getLogger(__name__) MANILA_SHARE_CONTAINER = "manila-share" CEPH_NFS_RELATION_NAME = "ceph-nfs" +MANILA_RELATION_NAME = "manila" +SHARE_PROTOCOL_NFS = "NFS" @sunbeam_tracing.trace_type @@ -140,6 +143,54 @@ class ManilaSharePebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type +class ManilaProvidesHandler(sunbeam_rhandlers.RelationHandler): + """Handler for manila relation.""" + + interface: "manila_k8s.ManilaProvides" + + def setup_event_handler(self): + """Configure event handlers for manila service relation.""" + logger.debug("Setting up manila event handler") + handler = sunbeam_tracing.trace_type(manila_k8s.ManilaProvides)( + self.charm, + self.relation_name, + ) + + self.framework.observe( + handler.on.manila_connected, + self._on_manila_connected, + ) + self.framework.observe( + handler.on.manila_goneaway, + self._on_manila_goneaway, + ) + + return handler + + def _on_manila_connected( + self, event: manila_k8s.ManilaConnectedEvent + ) -> None: + """Handle ManilaConnectedEvent event.""" + self.callback_f(event) + + def _on_manila_goneaway( + self, event: manila_k8s.ManilaGoneAwayEvent + ) -> None: + """Handle ManilaGoneAwayEvent event.""" + pass + + @property + def ready(self) -> bool: + """Report if relation is ready.""" + # This relation is not ready if there is no ceph-nfs relation. + relation = self.model.get_relation(CEPH_NFS_RELATION_NAME) + if not relation or not relation.data[relation.app].get("client"): + return False + + return True + + @sunbeam_tracing.trace_sunbeam_charm class ManilaShareCephfsCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" @@ -160,11 +211,30 @@ class ManilaShareCephfsCharm(sunbeam_charm.OSBaseOperatorCharmK8S): ) handlers.append(self.ceph_nfs) + if self.can_add_handler(MANILA_RELATION_NAME, handlers): + self.manila_handler = ManilaProvidesHandler( + self, + MANILA_RELATION_NAME, + self.handle_manila, + ) + handlers.append(self.manila_handler) + return handlers def handle_ceph_nfs(self, event: ops.framework.EventBase) -> None: """Handle the ceph-nfs relation changes.""" self.configure_charm(event) + self.handle_manila(event) + + def handle_manila(self, event: ops.framework.EventBase) -> None: + """Handle the manila relation data.""" + if self.ceph_nfs.ready: + self.manila_handler.interface.update_share_protocol( + SHARE_PROTOCOL_NFS + ) + else: + # ceph-nfs relation not ready, remove relation data, if set. + self.manila_handler.interface.update_share_protocol(None) @property def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]: diff --git a/charms/manila-cephfs-k8s/tests/unit/test_charm.py b/charms/manila-cephfs-k8s/tests/unit/test_charm.py index 00d2e46f..ba2d9728 100644 --- a/charms/manila-cephfs-k8s/tests/unit/test_charm.py +++ b/charms/manila-cephfs-k8s/tests/unit/test_charm.py @@ -17,6 +17,7 @@ """Unit tests for the Manila Share (Cephfs) K8s Operator charm.""" import charm +import charms.manila_k8s.v0.manila as manila_k8s import ops_sunbeam.test_utils as test_utils from ops import ( model, @@ -58,6 +59,24 @@ class TestManilaCephfsCharm(test_utils.CharmTestCase): self.harness = test_utils.get_harness( _ManilaCephfsCharm, container_calls=self.container_calls ) + + # clean up events that were dynamically defined, + # otherwise we get issues because they'll be redefined, + # which is not allowed. + from charms.data_platform_libs.v0.data_interfaces import ( + DatabaseRequiresEvents, + ) + + for attr in ( + "database_database_created", + "database_endpoints_changed", + "database_read_only_endpoints_changed", + ): + try: + delattr(DatabaseRequiresEvents, attr) + except AttributeError: + pass + self.addCleanup(self.harness.cleanup) self.harness.begin() @@ -129,6 +148,14 @@ class TestManilaCephfsCharm(test_utils.CharmTestCase): self._file_exists("manila-share", "/etc/manila/manila.conf") ) + self.harness.add_relation("manila", "manila") + + # The ceph-nfs relation is not set yet, so there should not be any + # data here. + manila_rel = self.harness.model.get_relation("manila") + manila_rel_data = manila_rel.data[self.harness.model.app] + self.assertEqual({}, manila_rel_data) + ceph_rel_id = self.add_ceph_nfs_client_relation() # Now that the relation is added, we should have the ceph-related @@ -165,8 +192,19 @@ class TestManilaCephfsCharm(test_utils.CharmTestCase): manila_strings, ) + # After the ceph-nfs relation has been established, the charm should + # set the manila relation data. + self.assertEqual( + charm.SHARE_PROTOCOL_NFS, + manila_rel_data.get(manila_k8s.SHARE_PROTOCOL), + ) + # Remove the ceph-nfs relation. The relation handler should be in a # BlockedStatus. self.harness.remove_relation(ceph_rel_id) self.assertIsInstance(ceph_nfs_status.status, model.BlockedStatus) + + # Because the ceph-nfs relation has been removed, the manila relation + # data should be cleared. + self.assertEqual({}, manila_rel_data) diff --git a/charms/manila-k8s/README.md b/charms/manila-k8s/README.md index 17c02310..0f5ddd97 100644 --- a/charms/manila-k8s/README.md +++ b/charms/manila-k8s/README.md @@ -53,6 +53,7 @@ The following relations are optional: - `ingress-public`: To expose service on public network. - `logging`: To send logs to Loki. +- `manila`: To connect Manila with a storage backend. At least one is required. - `receive-ca-cert`: To enable TLS on the service endpoints. - `tracing`: To connect to a tracing backend. diff --git a/charms/manila-k8s/charmcraft.yaml b/charms/manila-k8s/charmcraft.yaml index a43eb0bc..1bbb882c 100644 --- a/charms/manila-k8s/charmcraft.yaml +++ b/charms/manila-k8s/charmcraft.yaml @@ -64,6 +64,8 @@ requires: interface: loki_push_api optional: true limit: 1 + manila: + interface: manila-backend receive-ca-cert: interface: certificate_transfer optional: true diff --git a/charms/manila-k8s/lib/charms/manila_k8s/v0/manila.py b/charms/manila-k8s/lib/charms/manila_k8s/v0/manila.py new file mode 100644 index 00000000..5a23d99b --- /dev/null +++ b/charms/manila-k8s/lib/charms/manila_k8s/v0/manila.py @@ -0,0 +1,196 @@ +"""Manila Provides and Requires module. + +This library contains the Requires and Provides classes for handling +the manila interface. + +Import `ManilaRequires` in your charm, with the charm object and the +relation name: + - self + - "manila" + +Two events are also available to respond to: + - connected + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.manila_k8s.v0.manila as manila_k8s + + +class ManilaClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # Manila Requires + self._manila = manila_k8s.ManilaRequires( + self, "manila", + ) + self.framework.observe( + self._manila.on.connected, + self._on_manila_connected, + ) + self.framework.observe( + self._manila.on.goneaway, + self._on_manila_goneaway, + ) + + def _on_manila_connected(self, event): + '''React to the ManilaConnectedEvent event. + + This event happens when the manila relation is added to the + model before information has been provided. + ''' + # Do something before the relation is complete. + pass + + def _on_manila_goneaway(self, event): + '''React to the ManilaGoneAwayEvent event. + + This event happens when manila relation is removed. + ''' + # manila relation has goneaway. Shutdown services if needed. + pass +``` +""" + +import logging +from typing import List + +from ops.charm import ( + CharmBase, + RelationBrokenEvent, + RelationChangedEvent, + RelationJoinedEvent, + RelationEvent, +) +from ops.framework import ( + EventSource, + Object, + ObjectEvents, +) +from ops.model import ( + Relation, +) + +# The unique Charmhub library identifier, never change it +LIBID = "c074a92802f74a6f8460ae1875707a02" + +# 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 + + +SHARE_PROTOCOL = "share_protocol" + + +class ManilaConnectedEvent(RelationEvent): + """manila connected event.""" + + pass + + +class ManilaGoneAwayEvent(RelationEvent): + """manila relation has gone-away event""" + + pass + + +class ManilaEvents(ObjectEvents): + """Events class for `on`.""" + + manila_connected = EventSource(ManilaConnectedEvent) + manila_goneaway = EventSource(ManilaGoneAwayEvent) + + +class ManilaProvides(Object): + """ManilaProvides class.""" + + on = ManilaEvents() + + 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_joined, + self._on_manila_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_manila_relation_broken, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_manila_relation_broken, + ) + + def _on_manila_relation_joined(self, event: RelationJoinedEvent): + """Handle manila relation joined.""" + logging.debug("manila relation joined") + self.on.manila_connected.emit(event.relation) + + def _on_manila_relation_broken(self, event: RelationBrokenEvent): + """Handle manila relation broken.""" + logging.debug("manila relation broken") + self.on.manila_goneaway.emit(event.relation) + + @property + def _manila_rel(self) -> Relation | None: + """The manila relation.""" + return self.framework.model.get_relation(self.relation_name) + + def update_share_protocol(self, share_protocol: str | None): + """Updates the share protocol in the manila relation.""" + + data = self._manila_rel.data[self.model.app] + if share_protocol: + data[SHARE_PROTOCOL] = share_protocol + else: + data.pop(SHARE_PROTOCOL, None) + + +class ManilaRequires(Object): + """ManilaRequires class.""" + + on = ManilaEvents() + + 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_manila_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_manila_relation_broken, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_manila_relation_broken, + ) + + def _on_manila_relation_changed(self, event: RelationChangedEvent): + """Handle manila relation changed.""" + logging.debug("manila relation changed") + self.on.manila_connected.emit(event.relation) + + def _on_manila_relation_broken(self, event: RelationBrokenEvent): + """Handle manila relation broken.""" + logging.debug("manila relation broken") + self.on.manila_goneaway.emit(event.relation) + + @property + def share_protocols(self) -> List[str]: + """Get the manila share protocols from the manila relations.""" + protocols = set() + for relation in self.model.relations[self.relation_name]: + app_data = relation.data[relation.app] + if app_data.get(SHARE_PROTOCOL): + protocols.add(app_data[SHARE_PROTOCOL]) + + return list(protocols) diff --git a/charms/manila-k8s/src/charm.py b/charms/manila-k8s/src/charm.py index 854d5c84..4791cb1a 100755 --- a/charms/manila-k8s/src/charm.py +++ b/charms/manila-k8s/src/charm.py @@ -21,15 +21,19 @@ This charm provides Manila services as part of an OpenStack deployment. import logging from typing import ( + Callable, Dict, List, Mapping, ) +import charms.manila_k8s.v0.manila as manila_k8s import ops import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.config_contexts as sunbeam_ctxts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.relation_handlers as sunbeam_rhandlers import ops_sunbeam.tracing as sunbeam_tracing logger = logging.getLogger(__name__) @@ -37,6 +41,21 @@ logger = logging.getLogger(__name__) MANILA_API_PORT = 8786 MANILA_API_CONTAINER = "manila-api" MANILA_SCHEDULER_CONTAINER = "manila-scheduler" +MANILA_RELATION_NAME = "manila" + + +@sunbeam_tracing.trace_type +class ManilaConfigurationContext(sunbeam_ctxts.ConfigContext): + """Configuration context to set manila parameters.""" + + def context(self) -> dict: + """Generate configuration information for manila config.""" + share_protocols = self.charm.manila_share.interface.share_protocols + ctxt = { + "enabled_share_protocols": ",".join(share_protocols), + } + + return ctxt @sunbeam_tracing.trace_type @@ -83,6 +102,77 @@ class ManilaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_type +class ManilaRequiresHandler(sunbeam_rhandlers.RelationHandler): + """Handles the manila relation on the requires side.""" + + def __init__( + self, + charm: ops.charm.CharmBase, + relation_name: str, + region: str, + callback_f: Callable, + ): + """Constructor for ManilaRequiresHandler. + + Creates a new ManilaRequiresHandler 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 region: the region the manila services are configured for + :type region: str + :param callback_f: the function to call when the nodes are connected + :type callback_f: Callable + """ + super().__init__(charm, relation_name, callback_f, True) + self.region = region + + def setup_event_handler(self): + """Configure event handlers for the manila service relation.""" + logger.debug("Setting up manila event handler") + manila_handler = sunbeam_tracing.trace_type(manila_k8s.ManilaRequires)( + self.charm, + self.relation_name, + ) + self.framework.observe( + manila_handler.on.manila_connected, + self._manila_connected, + ) + self.framework.observe( + manila_handler.on.manila_goneaway, + self._manila_goneaway, + ) + return manila_handler + + def _manila_connected(self, event) -> None: + """Handles manila connected events.""" + self.callback_f(event) + + def _manila_goneaway(self, event) -> None: + """Handles manila goneaway events.""" + self.callback_f(event) + + @property + def ready(self) -> bool: + """Interface ready for use.""" + relations = self.model.relations[self.relation_name] + + # We need at least one relation. + if not relations: + return False + + for relation in relations: + # All relations should have their data set. + if not relation.data[relation.app].get(manila_k8s.SHARE_PROTOCOL): + return False + + return True + + @sunbeam_tracing.trace_sunbeam_charm class ManilaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" @@ -178,6 +268,29 @@ class ManilaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): ] return pebble_handlers + def get_relation_handlers( + self, handlers: List[sunbeam_rhandlers.RelationHandler] = None + ) -> List[sunbeam_rhandlers.RelationHandler]: + """Relation handlers for the operator.""" + handlers = super().get_relation_handlers(handlers or []) + if self.can_add_handler(MANILA_RELATION_NAME, handlers): + self.manila_share = ManilaRequiresHandler( + self, + MANILA_RELATION_NAME, + self.model.config["region"], + self.configure_charm, + ) + handlers.append(self.manila_share) + + return handlers + + @property + def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]: + """Configuration contexts for the operator.""" + contexts = super().config_contexts + contexts.append(ManilaConfigurationContext(self, "manila_config")) + return contexts + @property def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: """Container configuration files for the service.""" @@ -197,10 +310,6 @@ class ManilaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): ] return _cconfigs - def set_config_from_event(self, event: ops.framework.EventBase) -> None: - """Set config in relation data.""" - pass - @property def db_sync_container_name(self) -> str: """Name of Container to run db sync from.""" diff --git a/charms/manila-k8s/src/templates/manila.conf.j2 b/charms/manila-k8s/src/templates/manila.conf.j2 index 0d1a9d85..d3ed56d3 100644 --- a/charms/manila-k8s/src/templates/manila.conf.j2 +++ b/charms/manila-k8s/src/templates/manila.conf.j2 @@ -11,6 +11,9 @@ debug = {{ options.debug }} transport_url = {{ amqp.transport_url }} auth_strategy = keystone +# enabled_share_protocols = NFS,CIFS +enabled_share_protocols = {{ manila_config.enabled_share_protocols }} + {% include "parts/section-database" %} {% include "parts/section-identity" %} diff --git a/charms/manila-k8s/tests/unit/test_charm.py b/charms/manila-k8s/tests/unit/test_charm.py index 12f28f59..36ea3690 100644 --- a/charms/manila-k8s/tests/unit/test_charm.py +++ b/charms/manila-k8s/tests/unit/test_charm.py @@ -17,7 +17,11 @@ """Unit tests for the Manila K8s charm.""" import charm +import charms.manila_k8s.v0.manila as manila_k8s import ops_sunbeam.test_utils as test_utils +from ops import ( + model, +) from ops.testing import ( Harness, ) @@ -91,6 +95,23 @@ class TestManilaOperatorCharm(test_utils.CharmTestCase): ) return rel_id + def add_manila_relation(self) -> int: + """Add the manila relation and unit data.""" + return self.harness.add_relation( + "manila", + "manila-cephfs", + app_data={manila_k8s.SHARE_PROTOCOL: "foo"}, + ) + + def _check_file_contents(self, container, path, strings): + client = self.harness.charm.unit.get_container(container)._pebble # type: ignore + + with client.pull(path) as infile: + received_data = infile.read() + + for string in strings: + self.assertIn(string, received_data) + def test_pebble_ready_handler(self): """Test pebble ready event handling.""" self.assertEqual(self.harness.charm.seen_events, []) @@ -106,6 +127,13 @@ class TestManilaOperatorCharm(test_utils.CharmTestCase): test_utils.add_all_relations(self.harness) test_utils.add_complete_ingress_relation(self.harness) + # manila is a required relation. + manila_share_status = self.harness.charm.manila_share.status + self.assertIsInstance(manila_share_status.status, model.BlockedStatus) + + # Add the manila relation. + manila_rel_id = self.add_manila_relation() + setup_cmds = [ ["a2ensite", "wsgi-manila-api"], ] @@ -134,3 +162,14 @@ class TestManilaOperatorCharm(test_utils.CharmTestCase): ] for f in config_files: self.check_file("manila-scheduler", f) + + for container_name in ["manila-api", "manila-scheduler"]: + self._check_file_contents( + container_name, + "/etc/manila/manila.conf", + ["enabled_share_protocols = foo"], + ) + + self.harness.remove_relation(manila_rel_id) + + self.assertIsInstance(manila_share_status.status, model.BlockedStatus) diff --git a/tests/all-k8s/smoke.yaml.j2 b/tests/all-k8s/smoke.yaml.j2 index 32d381a4..66452f92 100644 --- a/tests/all-k8s/smoke.yaml.j2 +++ b/tests/all-k8s/smoke.yaml.j2 @@ -551,6 +551,8 @@ relations: - manila-cephfs:amqp - - keystone:identity-credentials - manila-cephfs:identity-credentials +- - manila:manila + - manila-cephfs:manila - - mysql:database - cinder:database diff --git a/tests/all-k8s/tests.yaml b/tests/all-k8s/tests.yaml index 70404505..b722dbbd 100644 --- a/tests/all-k8s/tests.yaml +++ b/tests/all-k8s/tests.yaml @@ -104,8 +104,8 @@ target_deploy_status: workload-status: blocked workload-status-message-regex: '^.*Configuration parameter kubeconfig not set$' manila: - workload-status: active - workload-status-message-regex: '^$' + workload-status: waiting + workload-status-message-regex: '^.*Not all relations are ready$' manila-cephfs: workload-status: waiting workload-status-message-regex: '^.*Not all relations are ready$' diff --git a/tests/local/zaza/sunbeam/charm_tests/keystone/setup.py b/tests/local/zaza/sunbeam/charm_tests/keystone/setup.py index e29e329b..218ecba8 100644 --- a/tests/local/zaza/sunbeam/charm_tests/keystone/setup.py +++ b/tests/local/zaza/sunbeam/charm_tests/keystone/setup.py @@ -27,6 +27,10 @@ SERVICE_CODES = { "gnocchi": [requests.codes.bad_gateway, requests.codes.service_unavailable], "heat-cfn": [requests.codes.bad_request], "heat": [requests.codes.bad_request], + "manilav2": [ + requests.codes.bad_gateway, + requests.codes.service_unavailable, + ], }