Restore use of address on OVN interfaces

Some clients can't use hostnames yet so expose both address and
hostname information on the ovsdb{-cms} interface type.

This supports backwards compatibility with the existing reactive
interfaces as well.

Change-Id: Iefe8ad601b305e3fdab7548e1d3357493491d3e9
This commit is contained in:
James Page 2022-10-24 10:25:26 +01:00
parent 9c552e6b3a
commit 193fe99c40
3 changed files with 125 additions and 21 deletions

View File

@ -14,6 +14,7 @@
"""Base classes for defining OVN relation handlers."""
import ipaddress
import itertools
import socket
import logging
@ -37,6 +38,37 @@ class OVNRelationUtils():
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: 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
def _remote_hostnames(self, key: str) -> Iterator[str]:
"""Retrieve hostnames published by remote units.
@ -57,6 +89,15 @@ class OVNRelationUtils():
"""
return self._remote_hostnames('bound-hostname')
@property
def cluster_remote_addrs(self) -> Iterator[str]:
"""Retrieve remote addresses bound to remote endpoint.
:returns: addresses bound to remote endpoints.
:rtype: Iterator[str]
"""
return self._remote_addrs('bound-address')
def db_connection_strs(
self,
hostnames: List[str],
@ -132,19 +173,48 @@ class OVNRelationUtils():
:returns: OVN Northbound OVSDB connection strings.
:rtpye: Iterator[str]
"""
return self.db_connection_strs(self.cluster_remote_hostnames,
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 db_nb_connection_hostname_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_hostnames,
self.db_nb_port)
@property
def db_sb_connection_hostname_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_hostnames,
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()
@property
def cluster_local_hostname(self) -> str:
"""Retrieve local hostname for unit.
@ -154,6 +224,18 @@ class OVNRelationUtils():
"""
return socket.getfqdn()
def _endpoint_local_bound_addr(self) -> ipaddress.IPv4Address:
"""Retrieve local address bound to endpoint.
:returns: IPv4 or IPv6 address bound to endpoint
"""
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):
@ -325,8 +407,10 @@ class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler,
# Ready is only emitted when the interface considers
# that the relation is complete (indicated by a password)
# _hostname = hostname or self.cluster_local_hostname
self.interface.set_unit_data(
{'bound-hostname': str(self.cluster_local_hostname)})
self.interface.set_unit_data({
'bound-hostname': str(self.cluster_local_hostname),
'bound-address': str(self.cluster_local_addr),
})
self.callback_f(event)
@property
@ -379,7 +463,14 @@ class OVSDBCMSRequiresHandler(sunbeam_rhandlers.RelationHandler,
ctxt.update({
'local_hostname': self.cluster_local_hostname,
'hostnames': self.interface.bound_hostnames(),
'local_address': self.cluster_local_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)})
'db_nb_connection_strs': ','.join(self.db_nb_connection_strs),
'db_sb_connection_hostname_strs':
','.join(self.db_sb_connection_hostname_strs),
'db_nb_connection_hostname_strs':
','.join(self.db_nb_connection_hostname_strs)
})
return ctxt

View File

@ -37,7 +37,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1
LIBPATCH = 3
# TODO: add your code here! Happy coding!
@ -101,11 +101,14 @@ class OVSDBCMSRequires(Object):
logging.debug("OVSDBCMSRequires on_joined")
self.on.connected.emit()
def bound_hostnames(self):
return self.get_all_unit_values("bound-hostname")
def bound_addresses(self):
return self.get_all_unit_values("bound-address")
def remote_ready(self):
return all(self.bound_addresses())
return all(self.bound_hostnames()) or all(self.bound_addresses())
def _on_ovsdb_cms_relation_changed(self, event):
"""OVSDBCMS relation changed."""
@ -201,5 +204,3 @@ class OVSDBCMSProvides(Object):
for relation in relations:
for k, v in settings.items():
relation.data[self.model.unit][k] = v

View File

@ -54,7 +54,7 @@ class SomeCharm(CharmBase):
import logging
import socket
import typing
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Optional, Tuple, Union
import yaml
from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
@ -69,7 +69,7 @@ LIBAPI = 1
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 3
LIBPATCH = 5
DEFAULT_RELATION_NAME = "ingress"
RELATION_INTERFACE = "ingress"
@ -97,6 +97,7 @@ INGRESS_REQUIRES_APP_SCHEMA = {
"name": {"type": "string"},
"host": {"type": "string"},
"port": {"type": "string"},
"strip-prefix": {"type": "string"},
},
"required": ["model", "name", "host", "port"],
}
@ -115,7 +116,11 @@ except ImportError:
from typing_extensions import TypedDict # py35 compat
# Model of the data a unit implementing the requirer will need to provide.
RequirerData = TypedDict("RequirerData", {"model": str, "name": str, "host": str, "port": int})
RequirerData = TypedDict(
"RequirerData",
{"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool},
total=False,
)
# Provider ingress data model.
ProviderIngressData = TypedDict("ProviderIngressData", {"url": str})
# Provider application databag model.
@ -221,12 +226,14 @@ class _IPAEvent(RelationEvent):
class IngressPerAppDataProvidedEvent(_IPAEvent):
"""Event representing that ingress data has been provided for an app."""
__args__ = ("name", "model", "port", "host")
__args__ = ("name", "model", "port", "host", "strip_prefix")
if typing.TYPE_CHECKING:
name = None # type: str
model = None # type: str
port = None # type: int
host = None # type: str
strip_prefix = False # type: bool
class IngressPerAppDataRemovedEvent(RelationEvent):
@ -266,6 +273,7 @@ class IngressPerAppProvider(_IngressPerAppBase):
data["model"],
data["port"],
data["host"],
data.get("strip-prefix", False),
)
def _handle_relation_broken(self, event):
@ -297,17 +305,14 @@ class IngressPerAppProvider(_IngressPerAppBase):
return {}
databag = relation.data[relation.app]
try:
remote_data = {k: databag[k] for k in ("model", "name", "host", "port")}
except KeyError as e:
# incomplete data / invalid data
log.debug("error {}; ignoring...".format(e))
return {}
except TypeError as e:
raise DataValidationError("Error casting remote data: {}".format(e))
remote_data = {} # type: Dict[str, Union[int, str]]
for k in ("port", "host", "model", "name", "mode", "strip-prefix"):
v = databag.get(k)
if v is not None:
remote_data[k] = v
_validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA)
remote_data["port"] = int(remote_data["port"])
remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False))
return remote_data
def get_data(self, relation: Relation) -> RequirerData:
@ -408,6 +413,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
*,
host: str = None,
port: int = None,
strip_prefix: bool = False,
):
"""Constructor for IngressRequirer.
@ -422,6 +428,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
relation must be of interface type `ingress` and have "limit: 1")
host: Hostname to be used by the ingress provider to address the requiring
application; if unspecified, the default Kubernetes service name will be used.
strip_prefix: configure Traefik to strip the path prefix.
Request Args:
port: the port of the service
@ -429,6 +436,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
super().__init__(charm, relation_name)
self.charm: CharmBase = charm
self.relation_name = relation_name
self._strip_prefix = strip_prefix
self._stored.set_default(current_url=None)
@ -501,6 +509,10 @@ class IngressPerAppRequirer(_IngressPerAppBase):
"host": host,
"port": str(port),
}
if self._strip_prefix:
data["strip-prefix"] = "true"
_validate_data(data, INGRESS_REQUIRES_APP_SCHEMA)
self.relation.data[self.app].update(data)