Refactor bootstrap

Break up the configure_charm method into distinct phases each
with a dedicated method. This makes it much easier for a charm
specialising this class to customise and individual phase
without having to duplicate code.

To enable this the configure_charm method has been adapted to use
the guard function. This allows any method within the guard
context manager to throw a guard exception to halt the
configuration process and have the issue reflected in the
units workload status. This is more flexable approach then
the old method of having configuration steps return a
boolean.

Change-Id: I6b46bae1dc700f5e8b597374c660c0af9c069172
This commit is contained in:
Liam Young 2023-02-25 14:53:10 +00:00
parent 00930c7419
commit a85980f087
2 changed files with 73 additions and 45 deletions

View File

@ -58,6 +58,8 @@ import ops_sunbeam.compound_status as compound_status
import ops_sunbeam.config_contexts as sunbeam_config_contexts
import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.guard as sunbeam_guard
import ops_sunbeam.job_ctrl as sunbeam_job_ctrl
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
logger = logging.getLogger(__name__)
@ -81,10 +83,14 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
"by ops_sunbeam"
)
)
# unit_bootstrapped is stored in the local unit storage which is lost
# when the pod is replaced, so this will revert to False on charm
# upgrade or upgrade of the payload container.
self._state.set_default(unit_bootstrapped=False)
self.status = compound_status.Status("workload", priority=100)
self.status_pool = compound_status.StatusPool(self)
self.status_pool.add(self.status)
self._state.set_default(bootstrapped=False)
self.relation_handlers = self.get_relation_handlers()
self.bootstrap_status = compound_status.Status(
"bootstrap", priority=90
)
@ -93,7 +99,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
self.bootstrap_status.set(
MaintenanceStatus("Service not bootstrapped")
)
self.relation_handlers = self.get_relation_handlers()
self.pebble_handlers = self.get_pebble_handlers()
self.framework.observe(self.on.config_changed, self._on_config_changed)
@ -242,19 +247,22 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
if h.container_name in container_names
]
# flake8: noqa: C901
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Catchall handler to configure charm services."""
def check_leader_ready(self):
"""Check the leader is reporting as ready."""
if self.supports_peer_relation and not (
self.unit.is_leader() or self.is_leader_ready()
):
logging.debug("Leader not ready")
return
raise sunbeam_guard.WaitingExceptionError("Leader not ready")
def check_relation_handlers_ready(self):
"""Check all relation handlers are ready."""
if not self.relation_handlers_ready():
logging.debug("Aborting charm relations not ready")
return
raise sunbeam_guard.WaitingExceptionError(
"Not all relations are ready"
)
def init_container_services(self):
"""Run init on pebble handlers that are ready."""
for ph in self.pebble_handlers:
if ph.pebble_ready:
logging.debug(f"Running init for {ph.service_name}")
@ -264,32 +272,64 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
f"Not running init for {ph.service_name},"
" container not ready"
)
raise sunbeam_guard.WaitingExceptionError(
"Payload container not ready"
)
def check_pebble_handlers_ready(self):
"""Check pebble handlers are ready."""
for ph in self.pebble_handlers:
if not ph.service_ready:
logging.debug(
f"Aborting container {ph.service_name} service not ready"
)
return
if not self.bootstrapped():
if not self._do_bootstrap():
self._state.bootstrapped = False
logging.warning(
"Failed to bootstrap the service, event deferred"
raise sunbeam_guard.WaitingExceptionError(
"Container service not ready"
)
# Defer the event to re-trigger the bootstrap process
event.defer()
return
if self.unit.is_leader() and self.supports_peer_relation:
self.set_leader_ready()
# Add healthchecks to the plan
def configure_unit(self, event: ops.framework.EventBase) -> None:
"""Run configuration on this unit."""
self.check_leader_ready()
self.check_relation_handlers_ready()
self.init_container_services()
self.check_pebble_handlers_ready()
self.run_db_sync()
self._state.unit_bootstrapped = True
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.
"""
self.set_leader_ready()
def configure_app_non_leader(self, event):
"""Setup steps for a non-leader after leader has bootstrapped."""
if not self.bootstrapped:
raise sunbeam_guard.WaitingExceptionError("Leader not ready")
def configure_app(self, event):
"""Check on (and run if leader) app wide tasks."""
if self.unit.is_leader():
self.configure_app_leader(event)
else:
self.configure_app_non_leader(event)
def add_pebble_health_checks(self):
"""Add health checks for services in payload containers."""
for ph in self.pebble_handlers:
ph.add_healthchecks()
self.status.set(ActiveStatus(""))
self._state.bootstrapped = True
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Catchall handler to configure charm services."""
with sunbeam_guard.guard(self, "Bootstrapping"):
self.configure_unit(event)
self.configure_app(event)
self.bootstrap_status.set(ActiveStatus())
self.add_pebble_health_checks()
logger.info("Setting active status")
self.status.set(ActiveStatus(""))
@property
def supports_peer_relation(self) -> bool:
@ -387,7 +427,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
def bootstrapped(self) -> bool:
"""Determine whether the service has been bootstrapped."""
return self._state.bootstrapped
return self._state.unit_bootstrapped and self.is_leader_ready()
def leader_set(self, settings: dict = None, **kwargs) -> None:
"""Juju set data in peer data bag."""
@ -426,8 +466,9 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
if warnings:
for line in warnings.splitlines():
logger.warning("DB Sync Out: %s", line.strip())
logging.debug("Output from database sync: \n%s", out)
logging.debug("Output from database sync: \n%s", out)
@sunbeam_job_ctrl.run_once_per_unit("db-sync")
def run_db_sync(self) -> None:
"""Run DB sync to init DB.
@ -451,23 +492,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
"Not DB sync ran. Charm does not specify self.db_sync_cmds"
)
def _do_bootstrap(self) -> bool:
"""Perform bootstrap.
:return: Return True if bootstrap is success
:rtype: bool
"""
try:
self.run_db_sync()
self.bootstrap_status.set(ActiveStatus())
return True
except ops.pebble.ExecError as e:
logger.exception("Failed to bootstrap")
logger.error("Exited with code %d. Stderr:", e.exit_code)
for line in e.stderr.splitlines():
logger.error(" %s", line)
return False
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
"""Base class for OpenStack API operators."""

View File

@ -23,10 +23,14 @@ import time
from functools import (
wraps,
)
from typing import (
TYPE_CHECKING,
)
import ops.framework
import ops_sunbeam
if TYPE_CHECKING:
import ops_sunbeam.charm
logger = logging.getLogger(__name__)
@ -52,7 +56,7 @@ def run_once_per_unit(label):
def wrap(f):
@wraps(f)
def wrapped_f(
charm: ops_sunbeam.charm.OSBaseOperatorCharm, *args, **kwargs
charm: "ops_sunbeam.charm.OSBaseOperatorCharm", *args, **kwargs
):
"""Run once decorator.