Files
sunbeam-charms/charms/ovn-central-k8s/src/charm.py
Guillaume Boutry cb27776b43 [ops-sunbeam] Move handlers setup in post_init
Because handler setup was performed in __init__, this made using
attributes defined in constructor impossible to use in the setup
methods except by ensuring right order in object init. This, in
turn, added a lot of overhead.
Methods of an object should be able to use attributes defined in
the object constructor.

Move handler setup in __post_init__ hooks, called after object
initialiazation.
Remove constructors only calling super() constructor.
Re-order attribute definition to be called after super()
constructor.

Change-Id: Ieb9d23b1e4764e2c0e894c932b7584b5b7c38258
Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
2025-02-21 13:30:22 +01:00

497 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
# 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
"""OVN Central Operator Charm.
This charm provide Glance services as part of an OpenStack deployment
"""
import logging
from typing import (
List,
Mapping,
)
import charms.ovn_central_k8s.v0.ovsdb as ovsdb
import ops
import ops.charm
import ops.pebble
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.config_contexts as sunbeam_ctxts
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.guard as sunbeam_guard
import ops_sunbeam.ovn.config_contexts as ovn_ctxts
import ops_sunbeam.ovn.container_handlers as ovn_chandlers
import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
import ops_sunbeam.tracing as sunbeam_tracing
import ovn
import ovsdb as ch_ovsdb
import tenacity
from ops.framework import (
StoredState,
)
logger = logging.getLogger(__name__)
OVN_SB_DB_CONTAINER = "ovn-sb-db-server"
OVN_NB_DB_CONTAINER = "ovn-nb-db-server"
OVN_NORTHD_CONTAINER = "ovn-northd"
OVN_DB_CONTAINERS = [OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER]
@sunbeam_tracing.trace_type
class OVNNorthBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
"""Handler for North OVN DB."""
@property
def wrapper_script(self):
"""Wrapper script for managing OVN service."""
return "/root/ovn-northd-wrapper.sh"
@property
def status_command(self):
"""Status command for container."""
return "/usr/share/ovn/scripts/ovn-ctl status_northd"
@property
def service_description(self):
"""Description of service."""
return "OVN Northd"
def default_container_configs(self):
"""Config files for container."""
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
"/etc/ovn/ovn-northd-db-params.conf", "root", "root"
)
)
return _cc
@sunbeam_tracing.trace_type
class OVNNorthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
"""Handler for North-bound OVN DB."""
@property
def wrapper_script(self):
"""Wrapper script for managing OVN service."""
return "/root/ovn-nb-db-server-wrapper.sh"
@property
def status_command(self):
"""Status command for container."""
# This command always return 0 even if the DB service
# is not running, so adding healthcheck with tcp check
return "/usr/share/ovn/scripts/ovn-ctl status_ovsdb"
@property
def service_description(self):
"""Description of service."""
return "OVN North Bound DB"
def default_container_configs(self):
"""Config files for container."""
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
"/root/ovn-nb-cluster-join.sh", "root", "root"
)
)
return _cc
def get_healthcheck_layer(self) -> dict:
"""Health check pebble layer.
:returns: pebble health check layer configuration for OVN NB DB
:rtype: dict
"""
return {
"checks": {
"online": {
"override": "replace",
"level": "ready",
"tcp": {"port": 6641},
},
}
}
@sunbeam_tracing.trace_type
class OVNSouthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
"""Handler for South-bound OVN DB."""
@property
def wrapper_script(self):
"""Wrapper script for managing OVN service."""
return "/root/ovn-sb-db-server-wrapper.sh"
@property
def status_command(self):
"""Status command for container."""
# This command always return 0 even if the DB service
# is not running, so adding healthcheck with tcp check
return "/usr/share/ovn/scripts/ovn-ctl status_ovsdb"
@property
def service_description(self):
"""Description of service."""
return "OVN South Bound DB"
def default_container_configs(self):
"""Config files for container."""
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
"/root/ovn-sb-cluster-join.sh", "root", "root"
)
)
return _cc
def get_healthcheck_layer(self) -> dict:
"""Health check pebble layer.
:returns: pebble health check layer configuration for OVN SB DB
:rtype: dict
"""
return {
"checks": {
"online": {
"override": "replace",
"level": "ready",
"tcp": {"port": 6642},
},
}
}
@sunbeam_tracing.trace_sunbeam_charm
class OVNCentralOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"""Charm the service."""
_state = StoredState()
mandatory_relations = {"peers"}
def get_pebble_handlers(self):
"""Pebble handlers for all OVN containers."""
pebble_handlers = [
OVNNorthBPebbleHandler(
self,
OVN_NORTHD_CONTAINER,
"ovn-northd",
self.container_configs,
self.template_dir,
self.configure_charm,
),
OVNSouthBDBPebbleHandler(
self,
OVN_SB_DB_CONTAINER,
"ovn-sb-db-server",
self.container_configs,
self.template_dir,
self.configure_charm,
),
OVNNorthBDBPebbleHandler(
self,
OVN_NB_DB_CONTAINER,
"ovn-nb-db-server",
self.container_configs,
self.template_dir,
self.configure_charm,
),
]
return pebble_handlers
def get_relation_handlers(
self, handlers=None
) -> List[sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = handlers or []
if self.can_add_handler("peers", handlers):
self.peers = ovn_rhandlers.OVNDBClusterPeerHandler(
self,
"peers",
self.configure_charm,
"peers" in self.mandatory_relations,
)
handlers.append(self.peers)
if self.can_add_handler("ovsdb-cms", handlers):
self.ovsdb_cms = ovn_rhandlers.OVSDBCMSProvidesHandler(
self,
"ovsdb-cms",
self.configure_charm,
"ovsdb-cms" in self.mandatory_relations,
)
handlers.append(self.ovsdb_cms)
handlers = super().get_relation_handlers(handlers)
return handlers
@property
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
"""Configuration contexts for the operator."""
contexts = super().config_contexts
contexts.append(ovn_ctxts.OVNDBConfigContext(self, "ovs_db"))
return contexts
@property
def databases(self) -> Mapping[str, str]:
"""Databases needed to support this charm.
Return empty dict as no mysql databases are
required.
"""
return {}
def ovn_rundir(self):
"""OVN run dir."""
return "/var/run/ovn"
def get_pebble_executor(self, container_name):
"""Execute command in pebble."""
container = self.unit.get_container(container_name)
def _run_via_pebble(*args):
process = container.exec(list(args), timeout=5 * 60)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning("CMD Out: %s", line.strip())
return out
return _run_via_pebble
@tenacity.retry(
stop=tenacity.stop_after_attempt(3),
retry=tenacity.retry_if_exception_type(ops.pebble.ExecError),
after=tenacity.after_log(logger, logging.WARNING),
wait=tenacity.wait_exponential(multiplier=1, min=5, max=30),
)
def cluster_status(self, db, cmd_executor):
"""OVN version agnostic cluster_status helper.
:param db: Database to operate on
:type db: str
:returns: Object describing the cluster status or None
:rtype: Optional[ch_ovn.OVNClusterStatus]
"""
try:
# The charm will attempt to retrieve cluster status before OVN
# is clustered and while units are paused, so we need to handle
# errors from this call gracefully.
return ovn.cluster_status(
db, rundir=self.ovn_rundir(), cmd_executor=cmd_executor
)
except ValueError as e:
logging.error(
"Unable to get cluster status, ovsdb-server "
"not ready yet?: {}".format(e)
)
return
def configure_ovn_listener(self, db, port_map):
"""Create or update OVN listener configuration.
:param db: Database to operate on, 'nb' or 'sb'
:type db: str
:param port_map: Dictionary with port number and associated settings
:type port_map: Dict[int,Dict[str,str]]
:raises: ValueError
"""
if db == "nb":
executor = self.get_pebble_executor(OVN_NB_DB_CONTAINER)
elif db == "sb":
executor = self.get_pebble_executor(OVN_SB_DB_CONTAINER)
status = self.cluster_status(
"ovn{}_db".format(db), cmd_executor=executor
)
if status and status.is_cluster_leader:
logging.debug(
"configure_ovn_listener is_cluster_leader {}".format(db)
)
connections = ch_ovsdb.SimpleOVSDB(
"ovn-{}ctl".format(db), cmd_executor=executor
).connection
for port, settings in port_map.items():
logging.debug("port {} {}".format(port, settings))
# discover and create any non-existing listeners first
for connection in connections.find(
'target="pssl:{}"'.format(port)
):
logging.debug("Found port {}".format(port))
break
else:
logging.debug("Create port {}".format(port))
executor(
"ovn-{}ctl".format(db),
"--",
"--id=@connection",
"create",
"connection",
'target="pssl:{}"'.format(port),
"--",
"add",
"{}_Global".format(db.upper()),
".",
"connections",
"@connection",
)
# set/update connection settings
for connection in connections.find(
'target="pssl:{}"'.format(port)
):
for k, v in settings.items():
logging.debug(
"set {} {} {}".format(
str(connection["_uuid"]), k, v
)
)
connections.set(str(connection["_uuid"]), k, v)
def check_leader_ready(self):
"""Check leader is ready and has supplied mandatory data."""
if self.supports_peer_relation and not (
self.unit.is_leader() or self.is_leader_ready()
):
raise sunbeam_guard.WaitingExceptionError("Leader not ready")
missing_leader_data = [
k for k in ["nb_cid", "sb_cid"] if not self.leader_get(k)
]
if missing_leader_data:
logging.debug(f"missing {missing_leader_data} from leader")
self.unit.status = ops.model.WaitingStatus(
"Waiting for data from leader"
)
raise sunbeam_guard.WaitingExceptionError(
"Missing data from leader"
)
def start_northd(self):
"""Start northd service."""
ph = self.get_named_pebble_handler(OVN_NORTHD_CONTAINER)
ph.start_service()
def configure_app_leader(self, event):
"""Run global app setup.
These are tasks that should only be run once per application and only
the leader runs them.
"""
# Start services in North/South containers on lead unit
logging.debug("Starting services in DB containers")
for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS):
ph.start_service()
# Attempt to setup listers etc
self.configure_ovn()
nb_status = self.cluster_status(
"ovnnb_db", self.get_pebble_executor(OVN_NB_DB_CONTAINER)
)
sb_status = self.cluster_status(
"ovnsb_db", self.get_pebble_executor(OVN_SB_DB_CONTAINER)
)
logging.debug("Telling peers leader is ready and cluster ids")
self.set_leader_ready()
self.leader_set(
{
"nb_cid": str(nb_status.cluster_id),
"sb_cid": str(sb_status.cluster_id),
}
)
self.set_leader_ready()
self.start_northd()
self.check_pebble_handlers_ready()
def configure_app_non_leader(self, event):
"""Configure non leader."""
if not self.peers.expected_peers_available():
raise sunbeam_guard.WaitingExceptionError(
"Expected peer units not ready, deferring cluster join"
)
logging.debug("Attempting to join OVN_Northbound cluster")
container = self.unit.get_container(OVN_NB_DB_CONTAINER)
process = container.exec(
["bash", "/root/ovn-nb-cluster-join.sh"], timeout=5 * 60
)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning("CMD Out: %s", line.strip())
logging.debug("Attempting to join OVN_Southbound cluster")
container = self.unit.get_container(OVN_SB_DB_CONTAINER)
process = container.exec(
["bash", "/root/ovn-sb-cluster-join.sh"], timeout=5 * 60
)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning("CMD Out: %s", line.strip())
logging.debug("Starting services in DB containers")
for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS):
ph.start_service()
# Attempt to setup listers etc
self.configure_ovn()
self.start_northd()
self.check_pebble_handlers_ready()
def configure_unit(self, event: ops.framework.EventBase) -> None:
"""Run configuration on this unit."""
self.check_leader_ready()
self.check_relation_handlers_ready(event)
self.open_ports()
self.init_container_services()
# Do not check_pebble_handlers_ready as northd is started later.
self._state.unit_bootstrapped = True
def open_ports(self):
"""Register ports in underlying cloud."""
self.unit.open_port("tcp", 6641)
self.unit.open_port("tcp", 6642)
def configure_ovn(self):
"""Configure ovn listener."""
inactivity_probe = (
int(self.config["ovsdb-server-inactivity-probe"]) * 1000
)
self.configure_ovn_listener(
"nb",
{
self.ovsdb_cms.db_nb_port: {
"inactivity_probe": inactivity_probe,
},
},
)
self.configure_ovn_listener(
"sb",
{
self.ovsdb_cms.db_sb_port: {
"inactivity_probe": inactivity_probe,
},
},
)
self.configure_ovn_listener(
"sb",
{
self.ovsdb_cms.db_sb_admin_port: {
"inactivity_probe": inactivity_probe,
},
},
)
if __name__ == "__main__": # pragma: nocover
ops.main(OVNCentralOperatorCharm)