Setup AMQP relation usage

This commit is contained in:
James Page 2021-09-27 10:39:40 +01:00
parent ec9d378d49
commit 0407b517d5
3 changed files with 302 additions and 15 deletions

View File

@ -0,0 +1,280 @@
"""RabbitMQAMQPProvides and Requires module.
This library contains the Requires and Provides classes for handling
the amqp interface.
Import `RabbitMQAMQPRequires` in your charm, with the charm object and the
relation name:
- self
- "amqp"
Also provide two additional parameters to the charm object:
- username
- vhost
Two events are also available to respond to:
- has_amqp_servers
- ready_amqp_servers
A basic example showing the usage of this relation follows:
```
from charms.sunbeam_rabbitmq_operator.v0.amqp import RabbitMQAMQPRequires
class AMQPClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# AMQP Requires
self.amqp_requires = RabbitMQAMQPRequires(
self, "amqp",
username = "amqp-client",
vhost = "amqp-client-vhost"
)
self.framework.observe(
self.amqp_requires.on.has_amqp_servers, self._on_has_amqp_servers)
self.framework.observe(
self.amqp_requires.on.ready_amqp_servers, self._on_ready_amqp_servers)
def _on_has_amqp_servers(self, event):
'''React to the AMQP relation joined.
The AMQP interface will use the provided username and vhost to commuicate
with the.
'''
# Do something before the relation is complete
def _on_ready_amqp_servers(self, event):
'''React to the AMQP relation joined.
The AMQP interface will use the provided username and vhost for the
request to the rabbitmq server.
'''
# AMQP Relation is ready. Do something with the completed relation.
```
"""
# The unique Charmhub library identifier, never change it
LIBID = "ab1414b6baf044f099caf9c117f1a101"
# 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 = 2
import logging
import requests
from ops.framework import (
StoredState,
EventBase,
ObjectEvents,
EventSource,
Object,
)
from ops.model import Relation
from typing import List
logger = logging.getLogger(__name__)
class HasAMQPServersEvent(EventBase):
"""Has AMQPServers Event."""
pass
class ReadyAMQPServersEvent(EventBase):
"""Ready AMQPServers Event."""
pass
class RabbitMQAMQPServerEvents(ObjectEvents):
"""Events class for `on`"""
has_amqp_servers = EventSource(HasAMQPServersEvent)
ready_amqp_servers = EventSource(ReadyAMQPServersEvent)
class RabbitMQAMQPRequires(Object):
"""
RabbitMQAMQPRequires class
"""
on = RabbitMQAMQPServerEvents()
_stored = StoredState()
def __init__(self, charm, relation_name: str, username: str, vhost: str):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.username = username
self.vhost = vhost
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):
"""AMQP relation joined."""
logging.debug("RabbitMQAMQPRequires on_joined")
self.on.has_amqp_servers.emit()
self.request_access(self.username, self.vhost)
def _on_amqp_relation_changed(self, event):
"""AMQP relation changed."""
logging.debug("RabbitMQAMQPRequires on_changed")
if self.password:
self.on.ready_amqp_servers.emit()
def _on_amqp_relation_broken(self, event):
"""AMQP relation broken."""
# TODO clear data on the relation
logging.debug("RabbitMQAMQPRequires on_departed")
@property
def _amqp_rel(self) -> Relation:
"""The AMQP relation."""
return self.framework.model.get_relation(self.relation_name)
@property
def password(self) -> str:
"""Return the AMQP password from the server side of the relation."""
return self._amqp_rel.data[self._amqp_rel.app].get("password")
@property
def hostname(self) -> str:
"""Return the hostname from the AMQP relation"""
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
@property
def hostnames(self) -> List[str]:
"""Return a list of remote RMQ hosts from the AMQP relation"""
_hosts = []
for unit in self._amqp_rel.units:
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
return _hosts
def request_access(self, username: str, vhost: str) -> None:
"""Request access to the AMQP server."""
if self.model.unit.is_leader():
logging.debug("Requesting AMQP user and vhost")
self._amqp_rel.data[self.charm.app]["username"] = username
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
class HasAMQPClientsEvent(EventBase):
"""Has AMQPClients Event."""
pass
class ReadyAMQPClientsEvent(EventBase):
"""AMQPClients Ready Event."""
pass
class RabbitMQAMQPClientEvents(ObjectEvents):
"""Events class for `on`"""
has_amqp_clients = EventSource(HasAMQPClientsEvent)
ready_amqp_clients = EventSource(ReadyAMQPClientsEvent)
class RabbitMQAMQPProvides(Object):
"""
RabbitMQAMQPProvides class
"""
on = RabbitMQAMQPClientEvents()
_stored = StoredState()
def __init__(self, charm, relation_name):
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_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):
"""Handle AMQP joined."""
logging.debug("RabbitMQAMQPProvides on_joined")
self.on.has_amqp_clients.emit()
def _on_amqp_relation_changed(self, event):
"""Handle AMQP changed."""
logging.debug("RabbitMQAMQPProvides on_changed")
# Validate data on the relation
if self.username(event) and self.vhost(event):
self.on.ready_amqp_clients.emit()
if self.charm.unit.is_leader():
self.set_amqp_credentials(
event, self.username(event), self.vhost(event)
)
def _on_amqp_relation_broken(self, event):
"""Handle AMQP broken."""
logging.debug("RabbitMQAMQPProvides on_departed")
# TODO clear data on the relation
def username(self, event):
"""Return the AMQP username from the client side of the relation."""
return event.relation.data[event.relation.app].get("username")
def vhost(self, event):
"""Return the AMQP vhost from the client side of the relation."""
return event.relation.data[event.relation.app].get("vhost")
def set_amqp_credentials(self, event, username, vhost):
"""Set AMQP Credentials.
:param event: The current event
:type EventsBase
:param username: The requested username
:type username: str
:param vhost: The requested vhost
:type vhost: str
:returns: None
:rtype: None
"""
# TODO: Can we move this into the charm code?
# TODO TLS Support. Existing interfaces set ssl_port and ssl_ca
logging.debug("Setting amqp connection information.")
try:
if not self.charm.does_vhost_exist(vhost):
self.charm.create_vhost(vhost)
password = self.charm.create_user(username)
self.charm.set_user_permissions(username, vhost)
event.relation.data[self.charm.app]["password"] = password
event.relation.data[self.charm.app][
"hostname"
] = self.charm.hostname
except requests.exceptions.ConnectionError as e:
logging.warning(
"Rabbitmq is not ready. Defering. Errno: {}".format(e.errno)
)
event.defer()

View File

@ -28,6 +28,8 @@ resources:
requires: requires:
amqp: amqp:
interface: rabbitmq interface: rabbitmq
ceph:
interface: ceph-client
provides: provides:
ceph-access: ceph-access:

View File

@ -6,15 +6,14 @@ This charm provide Cinder <-> Ceph integration as part of an OpenStack deploymen
import logging import logging
from ops.charm import CharmBase from ops.charm import CharmBase
from ops.framework import StoredState from ops.framework import StoredState
from ops.main import main from ops.main import main
from ops.model import ActiveStatus from ops.model import ActiveStatus
logger = logging.getLogger(__name__) from charms.sunbeam_rabbitmq_operator.v0.amqp import RabbitMQAMQPRequires
CINDER_API_PORT = 8090 logger = logging.getLogger(__name__)
class CinderCephOperatorCharm(CharmBase): class CinderCephOperatorCharm(CharmBase):
@ -25,18 +24,22 @@ class CinderCephOperatorCharm(CharmBase):
def __init__(self, *args): def __init__(self, *args):
super().__init__(*args) super().__init__(*args)
self.framework.observe(self.on.cinder_volume_pebble_ready, self.framework.observe(
self._on_cinder_volume_pebble_ready) self.on.cinder_volume_pebble_ready,
self._on_cinder_volume_pebble_ready,
)
self.framework.observe(self.on.config_changed, self.framework.observe(self.on.config_changed, self._on_config_changed)
self._on_config_changed)
self._stored.set_default(amqp_ready=False) self._stored.set_default(amqp_ready=False)
self._stored.set_default(ceph_ready=False) self._stored.set_default(ceph_ready=False)
# TODO self.amqp = RabbitMQAMQPRequires(
# Register AMQP consumer + events self, "amqp", username="cinder", vhost="openstack"
)
self.framework.observe(
self.amqp.on.ready_amqp_servers, self._on_amqp_ready
)
# TODO # TODO
# State modelling # State modelling
# AMQP + Ceph -> +Volume # AMQP + Ceph -> +Volume
@ -52,15 +55,17 @@ class CinderCephOperatorCharm(CharmBase):
"override": "replace", "override": "replace",
"summary": "Cinder Volume", "summary": "Cinder Volume",
"command": "cinder-volume --use-syslog", "command": "cinder-volume --use-syslog",
"startup": "enabled" "startup": "enabled",
} }
} },
} }
def _on_cinder_volume_pebble_ready(self, event): def _on_cinder_volume_pebble_ready(self, event):
"""Define and start a workload using the Pebble API.""" """Define and start a workload using the Pebble API."""
container = event.workload container = event.workload
container.add_layer("cinder-volume", self._pebble_cinder_scheduler_layer, combine=True) container.add_layer(
"cinder-volume", self._pebble_cinder_volume_layer, combine=True
)
container.autostart() container.autostart()
def _on_config_changed(self, _): def _on_config_changed(self, _):
@ -71,7 +76,7 @@ class CinderCephOperatorCharm(CharmBase):
def _on_amqp_ready(self, event): def _on_amqp_ready(self, event):
"""AMQP service ready for use""" """AMQP service ready for use"""
pass self._stored.amqp_ready = True
if __name__ == "__main__": if __name__ == "__main__":