
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
420 lines
14 KiB
Python
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
|