
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>
338 lines
10 KiB
Python
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"
|
|
)
|