Guillaume Boutry 4d4b4a41b0
[ops-sunbeam] Ensure external connectivity for machine charms
Machine charms need external connectivity to access services hosted on a
K8S substrate.

Ensure rabbitmq / ovn relay are access remotely for machine charms.

Closes-Bug: #2098974
Change-Id: Ifadb196dd6d60e33feab7dc0d835a7ea84444b9e
Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
2025-02-21 17:39:48 +01:00

338 lines
10 KiB
Python

"""RabbitMQProvides and Requires module.
This library contains the Requires and Provides classes for handling
the rabbitmq interface.
Import `RabbitMQRequires` in your charm, with the charm object and the
relation name:
- self
- "amqp"
Also provide two additional parameters to the charm object:
- username
- vhost
- external_connectivity: Optional, default False
Two events are also available to respond to:
- connected
- ready
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
class RabbitMQClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# RabbitMQ Requires
self.amqp = RabbitMQRequires(
self, "amqp",
username="myusername",
vhost="vhostname"
)
self.framework.observe(
self.amqp.on.connected, self._on_amqp_connected)
self.framework.observe(
self.amqp.on.ready, self._on_amqp_ready)
self.framework.observe(
self.amqp.on.goneaway, self._on_amqp_goneaway)
def _on_amqp_connected(self, event):
'''React to the RabbitMQ connected event.
This event happens when n RabbitMQ relation is added to the
model before credentials etc have been provided.
'''
# Do something before the relation is complete
pass
def _on_amqp_ready(self, event):
'''React to the RabbitMQ ready event.
The RabbitMQ interface will use the provided username and vhost for the
request to the rabbitmq server.
'''
# RabbitMQ Relation is ready. Do something with the completed relation.
pass
def _on_amqp_goneaway(self, event):
'''React to the RabbitMQ goneaway event.
This event happens when an RabbitMQ relation is removed.
'''
# RabbitMQ Relation has goneaway. shutdown services or suchlike
pass
```
"""
import json
import logging
import typing
import ops
# The unique Charmhub library identifier, never change it
LIBID = "45622352791142fd9cf87232e3bd6f2a"
# 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 = 3
logger = logging.getLogger(__name__)
class RabbitMQConnectedEvent(ops.EventBase):
"""RabbitMQ connected Event."""
pass
class RabbitMQReadyEvent(ops.EventBase):
"""RabbitMQ ready for use Event."""
pass
class RabbitMQGoneAwayEvent(ops.EventBase):
"""RabbitMQ relation has gone-away Event."""
pass
class RabbitMQServerEvents(ops.ObjectEvents):
"""Events class for `on`."""
connected = ops.EventSource(RabbitMQConnectedEvent)
ready = ops.EventSource(RabbitMQReadyEvent)
goneaway = ops.EventSource(RabbitMQGoneAwayEvent)
class RabbitMQRequires(ops.Object):
"""RabbitMQRequires class."""
on = RabbitMQServerEvents() # type: ignore
def __init__(
self,
charm,
relation_name: str,
username: str,
vhost: str,
external_connectivity: bool = False,
):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.username = username
self.vhost = vhost
self.external_connectivity = external_connectivity
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_amqp_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_departed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_amqp_relation_broken,
)
def _on_amqp_relation_joined(self, event: ops.RelationJoinedEvent):
logging.debug("RabbitMQRabbitMQRequires on_joined")
self.on.connected.emit()
self.request_access(
self.username, self.vhost, self.external_connectivity
)
def _on_amqp_relation_changed(
self, event: ops.RelationChangedEvent | ops.RelationDepartedEvent
):
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
if self.password:
self.on.ready.emit()
def _on_amqp_relation_broken(self, event: ops.RelationBrokenEvent):
logging.debug("RabbitMQRabbitMQRequires on_broken")
self.on.goneaway.emit()
@property
def _amqp_rel(self) -> ops.Relation | None:
"""The RabbitMQ relation."""
return self.framework.model.get_relation(self.relation_name)
def _get(self, key: str) -> str | None:
"""Return property from the RabbitMQ relation."""
rel = self._amqp_rel
if rel and rel.active:
return rel.data[rel.app].get(key)
return None
@property
def password(self) -> str | None:
"""Return the RabbitMQ password from the server side of the relation."""
return self._get("password")
@property
def hostname(self) -> str | None:
"""Return the hostname from the RabbitMQ relation."""
return self._get("hostname")
@property
def ssl_port(self) -> str | None:
"""Return the SSL port from the RabbitMQ relation."""
return self._get("ssl_port")
@property
def ssl_ca(self) -> str | None:
"""Return the SSL port from the RabbitMQ relation."""
return self._get("ssl_ca")
@property
def hostnames(self) -> list[str]:
"""Return a list of remote RMQ hosts from the RabbitMQ relation."""
_hosts: list[str] = []
rel = self._amqp_rel
if not rel:
return _hosts
for unit in rel.units:
if ingress := rel.data[unit].get("ingress-address"):
_hosts.append(ingress)
return _hosts
def request_access(
self, username: str, vhost: str, external_connectivity: bool
) -> None:
"""Request access to the RabbitMQ server."""
if (rel := self._amqp_rel) and self.model.unit.is_leader():
logging.debug("Requesting RabbitMQ user and vhost")
rel.data[self.charm.app]["username"] = username
rel.data[self.charm.app]["vhost"] = vhost
rel.data[self.charm.app]["external_connectivity"] = json.dumps(
external_connectivity
)
class HasRabbitMQClientsEvent(ops.RelationEvent):
"""Has RabbitMQClients Event."""
pass
class ReadyRabbitMQClientsEvent(ops.RelationEvent):
"""RabbitMQClients Ready Event."""
pass
class GoneAwayRabbitMQClientsEvent(ops.RelationEvent):
"""RabbitMQClients GoneAway Event."""
pass
class RabbitMQClientEvents(ops.ObjectEvents):
"""Events class for `on`."""
has_amqp_clients = ops.EventSource(HasRabbitMQClientsEvent)
ready_amqp_clients = ops.EventSource(ReadyRabbitMQClientsEvent)
gone_away_amqp_clients = ops.EventSource(GoneAwayRabbitMQClientsEvent)
class RabbitMQProvides(ops.Object):
"""RabbitMQProvides class."""
on = RabbitMQClientEvents() # type: ignore
def __init__(
self,
charm: ops.CharmBase,
relation_name: str,
callback: typing.Callable,
):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.callback = callback
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_amqp_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_amqp_relation_broken,
)
def _on_amqp_relation_joined(self, event: ops.RelationJoinedEvent):
"""Handle RabbitMQ joined."""
logging.debug(
"RabbitMQRabbitMQProvides on_joined data={}".format(
event.relation.data[event.relation.app]
)
)
self.on.has_amqp_clients.emit(event.relation)
def _on_amqp_relation_changed(self, event: ops.RelationChangedEvent):
"""Handle RabbitMQ changed."""
relation = event.relation
logging.debug(
"RabbitMQRabbitMQProvides on_changed data={}".format(
relation.data[relation.app]
)
)
# Validate data on the relation
if self.username(relation) and self.vhost(relation):
self.on.ready_amqp_clients.emit(relation)
if self.charm.unit.is_leader():
self.callback(
event,
self.username(relation),
self.vhost(relation),
self.external_connectivity(relation),
)
else:
logging.warning(
"Received RabbitMQ changed event without the "
"expected keys ('username', 'vhost') in the "
"application data bag. Incompatible charm in "
"other end of relation?"
)
def _on_amqp_relation_broken(self, event: ops.RelationBrokenEvent):
"""Handle RabbitMQ broken."""
logging.debug("RabbitMQRabbitMQProvides on_departed")
self.on.gone_away_amqp_clients.emit(event.relation)
def _get(self, relation: ops.Relation, key: str) -> str | None:
"""Return property from the RabbitMQ relation."""
return relation.data[relation.app].get(key)
def username(self, relation: ops.Relation) -> str | None:
"""Return the RabbitMQ username from the client side of the relation."""
return self._get(relation, "username")
def vhost(self, relation: ops.Relation) -> str | None:
"""Return the RabbitMQ vhost from the client side of the relation."""
return self._get(relation, "vhost")
def external_connectivity(self, relation: ops.Relation) -> bool:
"""Return the RabbitMQ external_connectivity from the client side of the relation."""
return json.loads(
self._get(relation, "external_connectivity") or "false"
)