sunbeam-charms/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py
Hemanth Nakkina 6a0d9ac42c Add mandatory flag to relation handlers
Following changes are done in this patch:
* Add mandatory flag to relations handler classes default
to False.
* Add mandatory_relations to charm base classes to list
all the mandatory relations.
* update relation_handlers_ready based on mandatory_relations

Change-Id: Ibc846461cf92a0a6501a15d03907c93ecdf90063
2022-09-22 13:27:08 +05:30

420 lines
14 KiB
Python

# Copyright 2022 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Base classes for defining OVN relation handlers."""
import ipaddress
import itertools
import logging
from typing import Callable, Dict, Iterator, List
import ops.charm
import ops.framework
from .. import relation_handlers as sunbeam_rhandlers
logger = logging.getLogger(__name__)
class OVNRelationUtils():
"""Common utilities for processing OVN relations."""
DB_NB_PORT = 6641
DB_SB_PORT = 6642
DB_SB_ADMIN_PORT = 16642
DB_NB_CLUSTER_PORT = 6643
DB_SB_CLUSTER_PORT = 6644
def _format_addr(self, addr: str) -> str:
"""Validate and format IP address.
:param addr: IPv6 or IPv4 address
:type addr: str
:returns: Address string, optionally encapsulated in brackets ([])
:rtype: str
:raises: ValueError
"""
ipaddr = ipaddress.ip_address(addr)
if isinstance(ipaddr, ipaddress.IPv6Address):
fmt = '[{}]'
else:
fmt = '{}'
return fmt.format(ipaddr)
def _remote_addrs(self, key: str) -> Iterator[str]:
"""Retrieve addresses published by remote units.
:param key: Relation data key to retrieve value from.
:type key: str
:returns: IPv4 or IPv6 addresses published by remote units.
:rtype: Iterator[str]
"""
for addr in self.interface.get_all_unit_values(key):
try:
addr = self._format_addr(addr)
yield addr
except ValueError:
continue
@property
def cluster_remote_addrs(self) -> Iterator[str]:
"""Retrieve remote addresses bound to remote endpoint.
:returns: IPv4 or IPv6 addresses bound to remote endpoints.
:rtype: Iterator[str]
"""
return self._remote_addrs('bound-address')
def db_connection_strs(
self,
addrs: List[ipaddress.IPv4Address],
port: int,
proto: str = 'ssl') -> Iterator[str]:
"""Provide connection strings.
:param addrs: List of addresses to include in conn strs
:type addrs: List[ipaddress.IPv4Address]
:param port: Port number
:type port: int
:param proto: Protocol
:type proto: str
:returns: connection strings
:rtype: Iterator[str]
"""
for addr in addrs:
yield ':'.join((proto, str(addr), str(port)))
@property
def db_nb_port(self) -> int:
"""Provide port number for OVN Northbound OVSDB.
:returns: port number for OVN Northbound OVSDB.
:rtype: int
"""
return self.DB_NB_PORT
@property
def db_sb_port(self) -> int:
"""Provide port number for OVN Southbound OVSDB.
:returns: port number for OVN Southbound OVSDB.
:rtype: int
"""
return self.DB_SB_PORT
@property
def db_sb_admin_port(self) -> int:
"""Provide admin port number for OVN Southbound OVSDB.
This is a special listener to allow ``ovn-northd`` to connect to an
endpoint without RBAC enabled as there is currently no RBAC profile
allowing ``ovn-northd`` to perform its work.
:returns: admin port number for OVN Southbound OVSDB.
:rtype: int
"""
return self.DB_SB_ADMIN_PORT
@property
def db_nb_cluster_port(self) -> int:
"""Provide port number for OVN Northbound OVSDB.
:returns port number for OVN Northbound OVSDB.
:rtype: int
"""
return self.DB_NB_CLUSTER_PORT
@property
def db_sb_cluster_port(self) -> int:
"""Provide port number for OVN Southbound OVSDB.
:returns: port number for OVN Southbound OVSDB.
:rtype: int
"""
return self.DB_SB_CLUSTER_PORT
@property
def db_nb_connection_strs(self) -> Iterator[str]:
"""Provide OVN Northbound OVSDB connection strings.
:returns: OVN Northbound OVSDB connection strings.
:rtpye: Iterator[str]
"""
return self.db_connection_strs(self.cluster_remote_addrs,
self.db_nb_port)
@property
def db_sb_connection_strs(self) -> Iterator[str]:
"""Provide OVN Southbound OVSDB connection strings.
:returns: OVN Southbound OVSDB connection strings.
:rtpye: Iterator[str]
"""
return self.db_connection_strs(self.cluster_remote_addrs,
self.db_sb_port)
@property
def cluster_local_addr(self) -> ipaddress.IPv4Address:
"""Retrieve local address bound to endpoint.
:returns: IPv4 or IPv6 address bound to endpoint
:rtype: str
"""
return self._endpoint_local_bound_addr()
def _endpoint_local_bound_addr(self) -> ipaddress.IPv4Address:
"""Retrieve local address bound to endpoint.
:returns: IPv4 or IPv6 address bound to endpoint
:rtype: str
"""
addr = None
for relation in self.charm.model.relations.get(self.relation_name, []):
binding = self.charm.model.get_binding(relation)
addr = binding.network.bind_address
break
return addr
class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
OVNRelationUtils):
"""Handle OVN peer relation."""
def publish_cluster_local_addr(
self,
addr: ipaddress.IPv4Address = None) -> Dict:
"""Announce address on relation.
This will be used by our peers and clients to build a connection
string to the remote cluster.
:param addr: Override address to announce.
:type addr: Optional[str]
"""
_addr = addr or self.cluster_local_addr
if _addr:
self.interface.set_unit_data({'bound-address': str(_addr)})
def expected_peers_available(self) -> bool:
"""Whether expected peers have joined and published data on peer rel.
NOTE: This does not work for the normal inter-charm relations, please
refer separate method for that in the shared interface library.
:returns: True if expected peers have joined and published data,
False otherwise.
:rtype: bool
"""
joined_units = self.interface.all_joined_units()
# Remove this unit from expected_peer_units count
expected_remote_units = self.interface.expected_peer_units() - 1
if len(joined_units) < expected_remote_units:
logging.debug(
f"Expected {expected_remote_units} but only {joined_units} "
"have joined so far")
return False
addresses = self.interface.get_all_unit_values('bound-address')
if all(addresses) < expected_remote_units:
logging.debug(
"Not all units have published a bound-address. Current "
f"address list: {addresses}")
return False
else:
logging.debug(
f"All expected peers are present. Addresses: {addresses}")
return True
@property
def db_nb_connection_strs(self) -> Iterator[str]:
"""Provide Northbound DB connection strings.
We override the parent property because for the peer relation
``cluster_remote_addrs`` does not contain self.
:returns: Northbound DB connection strings
:rtype: Iterator[str]
"""
return itertools.chain(
self.db_connection_strs((self.cluster_local_addr,),
self.db_nb_port),
self.db_connection_strs(self.cluster_remote_addrs,
self.db_nb_port))
@property
def db_nb_cluster_connection_strs(self) -> Iterator[str]:
"""Provide Northbound DB Cluster connection strings.
We override the parent property because for the peer relation
``cluster_remote_addrs`` does not contain self.
:returns: Northbound DB connection strings
:rtype: Iterator[str]
"""
return itertools.chain(
self.db_connection_strs((self.cluster_local_addr,),
self.db_nb_cluster_port),
self.db_connection_strs(self.cluster_remote_addrs,
self.db_nb_cluster_port))
@property
def db_sb_cluster_connection_strs(self) -> Iterator[str]:
"""Provide Southbound DB Cluster connection strings.
We override the parent property because for the peer relation
``cluster_remote_addrs`` does not contain self.
:returns: Southbound DB connection strings
:rtype: Iterator[str]
"""
return itertools.chain(
self.db_connection_strs((self.cluster_local_addr,),
self.db_sb_cluster_port),
self.db_connection_strs(self.cluster_remote_addrs,
self.db_sb_cluster_port))
@property
def db_sb_connection_strs(self) -> Iterator[str]:
"""Provide Southbound DB connection strings.
We override the parent property because for the peer relation
``cluster_remote_addrs`` does not contain self. We use a different
port for connecting to the SB DB as there is currently no RBAC profile
that provide the privileges ``ovn-northd`` requires to operate.
:returns: Southbound DB connection strings
:rtype: Iterator[str]
"""
return itertools.chain(
self.db_connection_strs((self.cluster_local_addr,),
self.db_sb_admin_port),
self.db_connection_strs(self.cluster_remote_addrs,
self.db_sb_admin_port))
def _on_peers_relation_joined(
self, event: ops.framework.EventBase) -> None:
"""Process peer joined event."""
self.publish_cluster_local_addr()
def context(self) -> dict:
"""Context from relation data."""
ctxt = super().context()
ctxt.update({
'cluster_local_addr': self.cluster_local_addr,
'cluster_remote_addrs': self.cluster_remote_addrs,
'db_nb_cluster_connection_strs':
self.db_nb_cluster_connection_strs,
'db_sb_cluster_connection_strs':
self.db_sb_cluster_connection_strs,
'db_sb_cluster_port': self.db_sb_cluster_port,
'db_nb_cluster_port': self.db_nb_cluster_port,
'db_nb_connection_strs': list(self.db_nb_connection_strs),
'db_sb_connection_strs': list(self.db_sb_connection_strs)})
return ctxt
class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler,
OVNRelationUtils):
"""Handle provides side of ovsdb-cms."""
def __init__(
self,
charm: ops.charm.CharmBase,
relation_name: str,
callback_f: Callable,
mandatory: bool = False,
) -> None:
"""Run constructor."""
super().__init__(charm, relation_name, callback_f, mandatory)
def setup_event_handler(self) -> ops.charm.Object:
"""Configure event handlers for an Identity service relation."""
# Lazy import to ensure this lib is only required if the charm
# has this relation.
logger.debug("Setting up ovs-cms provides event handler")
import charms.sunbeam_ovn_central_operator.v0.ovsdb as ovsdb
ovsdb_svc = ovsdb.OVSDBCMSProvides(
self.charm,
self.relation_name,
)
self.framework.observe(
ovsdb_svc.on.ready,
self._on_ovsdb_service_ready)
return ovsdb_svc
def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None:
"""Handle OVSDB CMS change events."""
# Ready is only emitted when the interface considers
# that the relation is complete (indicated by a password)
# _addr = addr or self.cluster_local_addr
self.interface.set_unit_data(
{
'bound-address': str(self.cluster_local_addr)})
self.callback_f(event)
@property
def ready(self) -> bool:
"""Whether the interface is ready."""
return True
class OVSDBCMSRequiresHandler(sunbeam_rhandlers.RelationHandler,
OVNRelationUtils):
"""Handle provides side of ovsdb-cms."""
def __init__(
self,
charm: ops.charm.CharmBase,
relation_name: str,
callback_f: Callable,
mandatory: bool = False,
) -> None:
"""Run constructor."""
super().__init__(charm, relation_name, callback_f, mandatory)
def setup_event_handler(self) -> ops.charm.Object:
"""Configure event handlers for an Identity service relation."""
# Lazy import to ensure this lib is only required if the charm
# has this relation.
logger.debug("Setting up ovs-cms requires event handler")
import charms.sunbeam_ovn_central_operator.v0.ovsdb as ovsdb
ovsdb_svc = ovsdb.OVSDBCMSRequires(
self.charm,
self.relation_name,
)
self.framework.observe(
ovsdb_svc.on.ready,
self._on_ovsdb_service_ready)
return ovsdb_svc
def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None:
"""Handle OVSDB CMS change events."""
self.callback_f(event)
@property
def ready(self) -> bool:
"""Whether the interface is ready."""
return self.interface.remote_ready()
def context(self) -> dict:
"""Context from relation data."""
ctxt = super().context()
ctxt.update({
'local_address': self._endpoint_local_bound_addr(),
'addresses': self.interface.bound_addresses(),
'db_sb_connection_strs': ','.join(self.db_sb_connection_strs),
'db_nb_connection_strs': ','.join(self.db_nb_connection_strs)})
return ctxt