Merge "Adds the manila relation" into main

This commit is contained in:
Zuul
2025-08-29 00:36:22 +00:00
committed by Gerrit Code Review
14 changed files with 480 additions and 6 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -60,6 +60,10 @@ requires:
optional: true
limit: 1
provides:
manila:
interface: manila-backend
parts:
update-certificates:
plugin: nil

View File

@@ -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]:

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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" %}

View File

@@ -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)

View File

@@ -551,6 +551,8 @@ relations:
- manila-cephfs:amqp
- - keystone:identity-credentials
- manila-cephfs:identity-credentials
- - manila:manila
- manila-cephfs:manila
- - mysql:database
- cinder:database

View File

@@ -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$'

View File

@@ -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,
],
}