Add linters and formatting

Change-Id: Ib16bd1fac4b9dfc21b98c9ba1201305733d2598b
This commit is contained in:
Liam Young
2022-11-03 09:50:52 +00:00
parent bc200fc470
commit 004e885760
25 changed files with 921 additions and 739 deletions

View File

@@ -8,7 +8,7 @@ needs to be applied to user supplied charm configuration. The context
has access to the charm object. has access to the charm object.
Below is an example which applies logic to the charm config as well as Below is an example which applies logic to the charm config as well as
collecting the application name to constuct the context. collecting the application name to construct the context.
.. code:: python .. code:: python

View File

@@ -15,7 +15,7 @@ Common Pebble handler changes
ASO provides a pebble handler base classes which provide the starting point ASO provides a pebble handler base classes which provide the starting point
for writing a new handler. If the container runs a service then the for writing a new handler. If the container runs a service then the
`ServicePebbleHandler` should be used. If the container does not provide a `ServicePebbleHandler` should be used. If the container does not provide a
service (perhaps its just an environment for executing commands that affact service (perhaps its just an environment for executing commands that effect
other container) then `PebbleHandler` should be used. other container) then `PebbleHandler` should be used.
.. code:: python .. code:: python
@@ -55,7 +55,7 @@ all available contexts.
'root', 'root',
'root')] 'root')]
If a service should be running in the conainer the handler specifies the If a service should be running in the container the handler specifies the
layer describing the service that will be passed to pebble. layer describing the service that will be passed to pebble.
.. code:: python .. code:: python

View File

@@ -51,7 +51,7 @@ an observer for relation changed events.
self.framework.observe(db_relation_event, self._on_database_changed) self.framework.observe(db_relation_event, self._on_database_changed)
return db return db
The method run when tha changed event is seen checks whether all required data The method runs when the changed event is seen checks whether all required data
has been provided. If it is then it calls back to the charm, if not then no has been provided. If it is then it calls back to the charm, if not then no
action is taken. action is taken.

View File

@@ -107,7 +107,7 @@ Test Service
============ ============
Check that the juju status shows the charms is active and no error messages are Check that the juju status shows the charms is active and no error messages are
preset. Then check the ironic api service is reponding. preset. Then check the ironic api service is responding.
.. code:: bash .. code:: bash

View File

@@ -31,17 +31,26 @@ containers and managing the service running in the container.
import ipaddress import ipaddress
import logging import logging
from typing import List, Mapping from typing import (
List,
Mapping,
)
import charms.observability_libs.v0.kubernetes_service_patch as kube_svc_patch
import ops.charm import ops.charm
import ops.framework import ops.framework
import ops.model import ops.model
import ops.pebble import ops.pebble
from lightkube import (
from ops.model import ActiveStatus, MaintenanceStatus Client,
)
from lightkube import Client from lightkube.resources.core_v1 import (
from lightkube.resources.core_v1 import Service Service,
)
from ops.model import (
ActiveStatus,
MaintenanceStatus,
)
import ops_sunbeam.compound_status as compound_status import ops_sunbeam.compound_status as compound_status
import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.config_contexts as sunbeam_config_contexts
@@ -49,8 +58,6 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.core as sunbeam_core import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.relation_handlers as sunbeam_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers
import charms.observability_libs.v0.kubernetes_service_patch as kube_svc_patch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,12 +78,13 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
self.status_pool.add(self.status) self.status_pool.add(self.status)
self._state.set_default(bootstrapped=False) self._state.set_default(bootstrapped=False)
self.bootstrap_status = compound_status.Status( self.bootstrap_status = compound_status.Status(
"bootstrap", "bootstrap", priority=90
priority=90) )
self.status_pool.add(self.bootstrap_status) self.status_pool.add(self.bootstrap_status)
if not self.bootstrapped(): if not self.bootstrapped():
self.bootstrap_status.set(MaintenanceStatus( self.bootstrap_status.set(
"Service not bootstrapped")) MaintenanceStatus("Service not bootstrapped")
)
self.relation_handlers = self.get_relation_handlers() self.relation_handlers = self.get_relation_handlers()
self.pebble_handlers = self.get_pebble_handlers() self.pebble_handlers = self.get_pebble_handlers()
self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.config_changed, self._on_config_changed)
@@ -145,9 +153,9 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
if self.can_add_handler("cloud-credentials", handlers): if self.can_add_handler("cloud-credentials", handlers):
self.ccreds = sunbeam_rhandlers.CloudCredentialsRequiresHandler( self.ccreds = sunbeam_rhandlers.CloudCredentialsRequiresHandler(
self, self,
'cloud-credentials', "cloud-credentials",
self.configure_charm, self.configure_charm,
'cloud-credentials' in self.mandatory_relations, "cloud-credentials" in self.mandatory_relations,
) )
handlers.append(self.ccreds) handlers.append(self.ccreds)
return handlers return handlers
@@ -162,25 +170,26 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
ip_sans = [] ip_sans = []
for relation_name in self.meta.relations.keys(): for relation_name in self.meta.relations.keys():
for relation in self.framework.model.relations.get( for relation in self.framework.model.relations.get(
relation_name, []): relation_name, []
):
binding = self.model.get_binding(relation) binding = self.model.get_binding(relation)
ip_sans.append(binding.network.ingress_address) ip_sans.append(binding.network.ingress_address)
ip_sans.append(binding.network.bind_address) ip_sans.append(binding.network.bind_address)
for binding_name in ['public']: for binding_name in ["public"]:
try: try:
binding = self.model.get_binding(binding_name) binding = self.model.get_binding(binding_name)
ip_sans.append(binding.network.ingress_address) ip_sans.append(binding.network.ingress_address)
ip_sans.append(binding.network.bind_address) ip_sans.append(binding.network.bind_address)
except ops.model.ModelError: except ops.model.ModelError:
logging.debug(f'No binding found for {binding_name}') logging.debug(f"No binding found for {binding_name}")
return ip_sans return ip_sans
def get_domain_name_sans(self) -> List[str]: def get_domain_name_sans(self) -> List[str]:
"""Get Domain names for service.""" """Get Domain names for service."""
domain_name_sans = [] domain_name_sans = []
for binding_config in ['admin', 'internal', 'public']: for binding_config in ["admin", "internal", "public"]:
hostname = self.config.get(f'os-{binding_config}-hostname') hostname = self.config.get(f"os-{binding_config}-hostname")
if hostname: if hostname:
domain_name_sans.append(hostname) domain_name_sans.append(hostname)
return domain_name_sans return domain_name_sans
@@ -200,8 +209,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
] ]
def get_named_pebble_handler( def get_named_pebble_handler(
self, self, container_name: str
container_name: str
) -> sunbeam_chandlers.PebbleHandler: ) -> sunbeam_chandlers.PebbleHandler:
"""Get pebble handler matching container_name.""" """Get pebble handler matching container_name."""
pebble_handlers = [ pebble_handlers = [
@@ -209,16 +217,16 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
for h in self.pebble_handlers for h in self.pebble_handlers
if h.container_name == container_name if h.container_name == container_name
] ]
assert len(pebble_handlers) < 2, ("Multiple pebble handlers with the " assert len(pebble_handlers) < 2, (
"same name found.") "Multiple pebble handlers with the " "same name found."
)
if pebble_handlers: if pebble_handlers:
return pebble_handlers[0] return pebble_handlers[0]
else: else:
return None return None
def get_named_pebble_handlers( def get_named_pebble_handlers(
self, self, container_names: List[str]
container_names: List[str]
) -> List[sunbeam_chandlers.PebbleHandler]: ) -> List[sunbeam_chandlers.PebbleHandler]:
"""Get pebble handlers matching container_names.""" """Get pebble handlers matching container_names."""
return [ return [
@@ -227,6 +235,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
if h.container_name in container_names if h.container_name in container_names
] ]
# flake8: noqa: C901
def configure_charm(self, event: ops.framework.EventBase) -> None: def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Catchall handler to configure charm services.""" """Catchall handler to configure charm services."""
if self.supports_peer_relation and not ( if self.supports_peer_relation and not (
@@ -246,19 +255,22 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
else: else:
logging.debug( logging.debug(
f"Not running init for {ph.service_name}," f"Not running init for {ph.service_name},"
" container not ready") " container not ready"
)
for ph in self.pebble_handlers: for ph in self.pebble_handlers:
if not ph.service_ready: if not ph.service_ready:
logging.debug( logging.debug(
f"Aborting container {ph.service_name} service not ready") f"Aborting container {ph.service_name} service not ready"
)
return return
if not self.bootstrapped(): if not self.bootstrapped():
if not self._do_bootstrap(): if not self._do_bootstrap():
self._state.bootstrapped = False self._state.bootstrapped = False
logging.warning( logging.warning(
"Failed to bootstrap the service, event deferred") "Failed to bootstrap the service, event deferred"
)
# Defer the event to re-trigger the bootstrap process # Defer the event to re-trigger the bootstrap process
event.defer() event.defer()
return return
@@ -306,8 +318,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
@property @property
def databases(self) -> Mapping[str, str]: def databases(self) -> Mapping[str, str]:
""" """Return a mapping of database relation names to database names.
Return a mapping of database relation names to database names.
Use this to define the databases required by an application. Use this to define the databases required by an application.
@@ -322,9 +333,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
It defaults to loading a relation named "database", It defaults to loading a relation named "database",
with the database named after the service name. with the database named after the service name.
""" """
return { return {"database": self.service_name.replace("-", "_")}
"database": self.service_name.replace("-", "_")
}
def _on_config_changed(self, event: ops.framework.EventBase) -> None: def _on_config_changed(self, event: ops.framework.EventBase) -> None:
self.configure_charm(event) self.configure_charm(event)
@@ -345,7 +354,8 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
if handler.mandatory and handler.ready if handler.mandatory and handler.ready
} }
not_ready_relations = self.mandatory_relations.difference( not_ready_relations = self.mandatory_relations.difference(
ready_relations) ready_relations
)
if len(not_ready_relations) != 0: if len(not_ready_relations) != 0:
logger.info(f"Relations {not_ready_relations} incomplete") logger.info(f"Relations {not_ready_relations} incomplete")
@@ -369,7 +379,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
return ra return ra
def bootstrapped(self) -> bool: def bootstrapped(self) -> bool:
"""Determine whether the service has been boostrapped.""" """Determine whether the service has been bootstrapped."""
return self._state.bootstrapped return self._state.bootstrapped
def leader_set(self, settings: dict = None, **kwargs) -> None: def leader_set(self, settings: dict = None, **kwargs) -> None:
@@ -401,24 +411,26 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
:raises: pebble.ExecError :raises: pebble.ExecError
""" """
if not self.unit.is_leader(): if not self.unit.is_leader():
logging.info('Not lead unit, skipping DB syncs') logging.info("Not lead unit, skipping DB syncs")
return return
try: try:
if self.db_sync_cmds: if self.db_sync_cmds:
logger.info("Syncing database...") logger.info("Syncing database...")
container = self.unit.get_container( container = self.unit.get_container(
self.db_sync_container_name) self.db_sync_container_name
)
for cmd in self.db_sync_cmds: for cmd in self.db_sync_cmds:
logging.debug('Running sync: \n%s', cmd) logging.debug("Running sync: \n%s", cmd)
process = container.exec(cmd, timeout=5*60) process = container.exec(cmd, timeout=5 * 60)
out, warnings = process.wait_output() out, warnings = process.wait_output()
if warnings: if warnings:
for line in warnings.splitlines(): for line in warnings.splitlines():
logger.warning('DB Sync Out: %s', line.strip()) 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)
except AttributeError: except AttributeError:
logger.warning( logger.warning(
"Not DB sync ran. Charm does not specify self.db_sync_cmds") "Not DB sync ran. Charm does not specify self.db_sync_cmds"
)
def _do_bootstrap(self) -> bool: def _do_bootstrap(self) -> bool:
"""Perform bootstrap. """Perform bootstrap.
@@ -431,21 +443,17 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
self.bootstrap_status.set(ActiveStatus()) self.bootstrap_status.set(ActiveStatus())
return True return True
except ops.pebble.ExecError as e: except ops.pebble.ExecError as e:
logger.exception('Failed to bootstrap') logger.exception("Failed to bootstrap")
logger.error('Exited with code %d. Stderr:', e.exit_code) logger.error("Exited with code %d. Stderr:", e.exit_code)
for line in e.stderr.splitlines(): for line in e.stderr.splitlines():
logger.error(' %s', line) logger.error(" %s", line)
return False return False
class OSBaseOperatorAPICharm(OSBaseOperatorCharm): class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
"""Base class for OpenStack API operators.""" """Base class for OpenStack API operators."""
mandatory_relations = { mandatory_relations = {"database", "identity-service", "ingress-public"}
'database',
'identity-service',
'ingress-public'
}
def __init__(self, framework: ops.framework.Framework) -> None: def __init__(self, framework: ops.framework.Framework) -> None:
"""Run constructor.""" """Run constructor."""
@@ -507,11 +515,13 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
update the relevant endpoints with the identity service, and then update the relevant endpoints with the identity service, and then
call the configure_charm. call the configure_charm.
""" """
logger.debug('Received an ingress_changed event') logger.debug("Received an ingress_changed event")
try: try:
if self.id_svc.update_service_endpoints: if self.id_svc.update_service_endpoints:
logger.debug('Updating service endpoints after ingress ' logger.debug(
'relation changed.') "Updating service endpoints after ingress "
"relation changed."
)
self.id_svc.update_service_endpoints(self.service_endpoints) self.id_svc.update_service_endpoints(self.service_endpoints)
except AttributeError: except AttributeError:
pass pass
@@ -540,17 +550,20 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
if load_balancer_status: if load_balancer_status:
ingress_addresses = load_balancer_status.ingress ingress_addresses = load_balancer_status.ingress
if ingress_addresses: if ingress_addresses:
logger.debug('Found ingress addresses on loadbalancer ' logger.debug(
'status') "Found ingress addresses on loadbalancer " "status"
)
ingress_address = ingress_addresses[0] ingress_address = ingress_addresses[0]
addr = ingress_address.hostname or ingress_address.ip addr = ingress_address.hostname or ingress_address.ip
if addr: if addr:
logger.debug('Using ingress address from loadbalancer ' logger.debug(
f'as {addr}') "Using ingress address from loadbalancer "
f"as {addr}"
)
return ingress_address.hostname or ingress_address.ip return ingress_address.hostname or ingress_address.ip
hostname = self.model.get_binding( hostname = self.model.get_binding(
'identity-service' "identity-service"
).network.ingress_address ).network.ingress_address
return hostname return hostname
@@ -559,9 +572,11 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
"""Url for accessing the public endpoint for this service.""" """Url for accessing the public endpoint for this service."""
try: try:
if self.ingress_public.url: if self.ingress_public.url:
logger.debug('Ingress-public relation found, returning ' logger.debug(
'ingress-public.url of: %s', "Ingress-public relation found, returning "
self.ingress_public.url) "ingress-public.url of: %s",
self.ingress_public.url,
)
return self.ingress_public.url return self.ingress_public.url
except AttributeError: except AttributeError:
pass pass
@@ -581,9 +596,11 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
"""Url for accessing the internal endpoint for this service.""" """Url for accessing the internal endpoint for this service."""
try: try:
if self.ingress_internal.url: if self.ingress_internal.url:
logger.debug('Ingress-internal relation found, returning ' logger.debug(
'ingress_internal.url of: %s', "Ingress-internal relation found, returning "
self.ingress_internal.url) "ingress_internal.url of: %s",
self.ingress_internal.url,
)
return self.ingress_internal.url return self.ingress_internal.url
except AttributeError: except AttributeError:
pass pass
@@ -669,4 +686,4 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
@property @property
def healthcheck_http_url(self) -> str: def healthcheck_http_url(self) -> str:
"""Healthcheck HTTP URL for the service.""" """Healthcheck HTTP URL for the service."""
return f'http://localhost:{self.default_public_ingress_port}/' return f"http://localhost:{self.default_public_ingress_port}/"

View File

@@ -1,5 +1,18 @@
""" # Copyright 2022 Canonical Ltd.
A mini library for tracking status messages. #
# 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.
"""A mini library for tracking status messages.
We want this because keeping track of everything We want this because keeping track of everything
with a single unit.status is too difficult. with a single unit.status is too difficult.
@@ -11,12 +24,31 @@ aspects of the application without clobbering other parts.
""" """
import json import json
import logging import logging
from typing import Callable, Dict, Tuple, Optional from typing import (
Callable,
Dict,
Optional,
Tuple,
)
from ops.charm import CharmBase from ops.charm import (
from ops.framework import Handle, Object, StoredStateData, CommitEvent CharmBase,
from ops.model import ActiveStatus, StatusBase, UnknownStatus, WaitingStatus )
from ops.storage import NoSnapshotError from ops.framework import (
CommitEvent,
Handle,
Object,
StoredStateData,
)
from ops.model import (
ActiveStatus,
StatusBase,
UnknownStatus,
WaitingStatus,
)
from ops.storage import (
NoSnapshotError,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,8 +62,7 @@ STATUS_PRIORITIES = {
class Status: class Status:
""" """An atomic status.
An atomic status.
A wrapper around a StatusBase from ops, A wrapper around a StatusBase from ops,
that adds a priority, label, that adds a priority, label,
@@ -39,8 +70,7 @@ class Status:
""" """
def __init__(self, label: str, priority: int = 0) -> None: def __init__(self, label: str, priority: int = 0) -> None:
""" """Create a new Status object.
Create a new Status object.
label: string label label: string label
priority: integer, higher number is higher priority, default is 0 priority: integer, higher number is higher priority, default is 0
@@ -59,8 +89,7 @@ class Status:
self.on_update: Optional[Callable[[], None]] = None self.on_update: Optional[Callable[[], None]] = None
def set(self, status: StatusBase) -> None: def set(self, status: StatusBase) -> None:
""" """Set the status.
Set the status.
Will also run the on_update hook if available Will also run the on_update hook if available
(should be set by the pool so the pool knows when it should update). (should be set by the pool so the pool knows when it should update).
@@ -71,8 +100,7 @@ class Status:
self.on_update() self.on_update()
def message(self) -> str: def message(self) -> str:
""" """Get the status message consistently.
Get the status message consistently.
Useful because UnknownStatus has no message attribute. Useful because UnknownStatus has no message attribute.
""" """
@@ -81,8 +109,7 @@ class Status:
return self.status.message return self.status.message
def priority(self) -> Tuple[int, int]: def priority(self) -> Tuple[int, int]:
""" """Return a value to use for sorting statuses by priority.
Return a value to use for sorting statuses by priority.
Used by the pool to retrieve the highest priority status Used by the pool to retrieve the highest priority status
to display to the user. to display to the user.
@@ -98,16 +125,14 @@ class Status:
class StatusPool(Object): class StatusPool(Object):
""" """A pool of Status objects.
A pool of Status objects.
This is implemented as an `Object`, This is implemented as an `Object`,
so we can more simply save state between hook executions. so we can more simply save state between hook executions.
""" """
def __init__(self, charm: CharmBase) -> None: def __init__(self, charm: CharmBase) -> None:
""" """Init the status pool and restore from stored state if available.
Init the status pool and restore from stored state if available.
Note that instantiating more than one StatusPool here is not supported, Note that instantiating more than one StatusPool here is not supported,
due to hardcoded framework stored data IDs. due to hardcoded framework stored data IDs.
@@ -143,15 +168,14 @@ class StatusPool(Object):
charm.framework.observe(charm.framework.on.commit, self._on_commit) charm.framework.observe(charm.framework.on.commit, self._on_commit)
def add(self, status: Status) -> None: def add(self, status: Status) -> None:
""" """Idempotently add a status object to the pool.
Idempotently add a status object to the pool.
Reconstitute from saved state if it's a new status. Reconstitute from saved state if it's a new status.
""" """
if ( if (
status.never_set and status.never_set
status.label in self._status_state and and status.label in self._status_state
status.label not in self._pool and status.label not in self._pool
): ):
# If this status hasn't been seen or set yet, # If this status hasn't been seen or set yet,
# and we have saved state for it, # and we have saved state for it,
@@ -168,24 +192,24 @@ class StatusPool(Object):
self.on_update() self.on_update()
def summarise(self) -> str: def summarise(self) -> str:
""" """Return a human readable summary of all the statuses in the pool.
Return a human readable summary of all the statuses in the pool.
Will be a multi-line string. Will be a multi-line string.
""" """
lines = [] lines = []
for status in sorted(self._pool.values(), key=lambda x: x.priority()): for status in sorted(self._pool.values(), key=lambda x: x.priority()):
lines.append("{label:>30}: {status:>10} | {message}".format( lines.append(
label=status.label, "{label:>30}: {status:>10} | {message}".format(
message=status.message(), label=status.label,
status=status.status.name, message=status.message(),
)) status=status.status.name,
)
)
return "\n".join(lines) return "\n".join(lines)
def _on_commit(self, _event: CommitEvent) -> None: def _on_commit(self, _event: CommitEvent) -> None:
""" """Store the current state of statuses.
Store the current state of statuses.
So we can restore them on the next run of the charm. So we can restore them on the next run of the charm.
""" """
@@ -199,8 +223,7 @@ class StatusPool(Object):
self._charm.framework._storage.commit() self._charm.framework._storage.commit()
def on_update(self) -> None: def on_update(self) -> None:
""" """Update the unit status with the current highest priority status.
Update the unit status with the current highest priority status.
Use as a hook to run whenever a status is updated in the pool. Use as a hook to run whenever a status is updated in the pool.
""" """
@@ -223,5 +246,5 @@ class StatusPool(Object):
"({}){}".format( "({}){}".format(
status.label, status.label,
" " + message if message else "", " " + message if message else "",
) ),
) )

View File

@@ -19,9 +19,14 @@ create reusable contexts which translate charm config, deployment state etc.
These are not specific to a relation. These are not specific to a relation.
""" """
from __future__ import annotations from __future__ import (
annotations,
)
import logging import logging
from typing import TYPE_CHECKING from typing import (
TYPE_CHECKING,
)
if TYPE_CHECKING: if TYPE_CHECKING:
import ops_sunbeam.charm import ops_sunbeam.charm
@@ -30,7 +35,7 @@ logger = logging.getLogger(__name__)
# XXX Dulpicating definition in relation handlers # XXX Dulpicating definition in relation handlers
ERASURE_CODED = "erasure-coded" ERASURE_CODED = "erasure-coded"
REPLICATED = "replacated" REPLICATED = "replicated"
class ConfigContext: class ConfigContext:
@@ -71,7 +76,7 @@ class WSGIWorkerConfigContext(ConfigContext):
def context(self) -> dict: def context(self) -> dict:
"""WSGI configuration options.""" """WSGI configuration options."""
log_svc_name = self.charm.service_name.replace('-', '_') log_svc_name = self.charm.service_name.replace("-", "_")
return { return {
"name": self.charm.service_name, "name": self.charm.service_name,
"public_port": self.charm.default_public_ingress_port, "public_port": self.charm.default_public_ingress_port,
@@ -105,21 +110,20 @@ class CinderCephConfigurationContext(ConfigContext):
def context(self) -> None: def context(self) -> None:
"""Cinder Ceph configuration context.""" """Cinder Ceph configuration context."""
config = self.charm.model.config.get config = self.charm.model.config.get
data_pool_name = config('rbd-pool-name') or self.charm.app.name data_pool_name = config("rbd-pool-name") or self.charm.app.name
if config('pool-type') == ERASURE_CODED: if config("pool-type") == ERASURE_CODED:
pool_name = ( pool_name = (
config('ec-rbd-metadata-pool') or config("ec-rbd-metadata-pool") or f"{data_pool_name}-metadata"
f"{data_pool_name}-metadata"
) )
else: else:
pool_name = data_pool_name pool_name = data_pool_name
backend_name = config('volume-backend-name') or self.charm.app.name backend_name = config("volume-backend-name") or self.charm.app.name
# TODO: # TODO:
# secret_uuid needs to be generated and shared for the app # secret_uuid needs to be generated and shared for the app
return { return {
'cluster_name': self.charm.app.name, "cluster_name": self.charm.app.name,
'rbd_pool': pool_name, "rbd_pool": pool_name,
'rbd_user': self.charm.app.name, "rbd_user": self.charm.app.name,
'backend_name': backend_name, "backend_name": backend_name,
'backend_availability_zone': config('backend-availability-zone'), "backend_availability_zone": config("backend-availability-zone"),
} }

View File

@@ -21,17 +21,25 @@ in the container.
import collections import collections
import logging import logging
from collections.abc import (
Callable,
)
from typing import (
List,
TypedDict,
)
import ops.charm
import ops.pebble
from ops.model import (
ActiveStatus,
BlockedStatus,
WaitingStatus,
)
import ops_sunbeam.compound_status as compound_status import ops_sunbeam.compound_status as compound_status
import ops_sunbeam.core as sunbeam_core import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.templating as sunbeam_templating import ops_sunbeam.templating as sunbeam_templating
import ops.charm
import ops.pebble
from ops.model import ActiveStatus, WaitingStatus, BlockedStatus
from collections.abc import Callable
from typing import List, TypedDict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,8 +81,7 @@ class PebbleHandler(ops.charm.Object):
self.charm.status_pool.add(self.status) self.charm.status_pool.add(self.status)
self.framework.observe( self.framework.observe(
self.charm.on.update_status, self.charm.on.update_status, self._on_update_status
self._on_update_status
) )
def setup_pebble_handler(self) -> None: def setup_pebble_handler(self) -> None:
@@ -150,10 +157,8 @@ class PebbleHandler(ops.charm.Object):
for d in self.directories: for d in self.directories:
logging.debug(f"Creating {d.path}") logging.debug(f"Creating {d.path}")
container.make_dir( container.make_dir(
d.path, d.path, user=d.user, group=d.group, make_parents=True
user=d.user, )
group=d.group,
make_parents=True)
def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None: def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None:
"""Initialise service ready for use. """Initialise service ready for use.
@@ -179,9 +184,7 @@ class PebbleHandler(ops.charm.Object):
@property @property
def pebble_ready(self) -> bool: def pebble_ready(self) -> bool:
"""Determine if pebble is running and ready for use.""" """Determine if pebble is running and ready for use."""
return self.charm.unit.get_container( return self.charm.unit.get_container(self.container_name).can_connect()
self.container_name
).can_connect()
@property @property
def config_pushed(self) -> bool: def config_pushed(self) -> bool:
@@ -193,8 +196,9 @@ class PebbleHandler(ops.charm.Object):
"""Determine whether the service the container provides is running.""" """Determine whether the service the container provides is running."""
return self._state.service_ready return self._state.service_ready
def execute(self, cmd: List, exception_on_error: bool = False, def execute(
**kwargs: TypedDict) -> str: self, cmd: List, exception_on_error: bool = False, **kwargs: TypedDict
) -> str:
"""Execute given command in container managed by this handler. """Execute given command in container managed by this handler.
:param cmd: command to execute, specified as a list of strings :param cmd: command to execute, specified as a list of strings
@@ -211,15 +215,15 @@ class PebbleHandler(ops.charm.Object):
stdout, _ = process.wait_output() stdout, _ = process.wait_output()
# Not logging the command in case it included a password, # Not logging the command in case it included a password,
# too cautious ? # too cautious ?
logger.debug('Command complete') logger.debug("Command complete")
if stdout: if stdout:
for line in stdout.splitlines(): for line in stdout.splitlines():
logger.debug(' %s', line) logger.debug(" %s", line)
return stdout return stdout
except ops.pebble.ExecError as e: except ops.pebble.ExecError as e:
logger.error('Exited with code %d. Stderr:', e.exit_code) logger.error("Exited with code %d. Stderr:", e.exit_code)
for line in e.stderr.splitlines(): for line in e.stderr.splitlines():
logger.error(' %s', line) logger.error(" %s", line)
if exception_on_error: if exception_on_error:
raise raise
@@ -236,7 +240,8 @@ class PebbleHandler(ops.charm.Object):
if not plan.checks: if not plan.checks:
logger.debug("Adding healthcheck layer to the plan") logger.debug("Adding healthcheck layer to the plan")
container.add_layer( container.add_layer(
"healthchecks", healthcheck_layer, combine=True) "healthchecks", healthcheck_layer, combine=True
)
except ops.pebble.ConnectionError as connect_error: except ops.pebble.ConnectionError as connect_error:
logger.error("Not able to add Healthcheck layer") logger.error("Not able to add Healthcheck layer")
logger.exception(connect_error) logger.exception(connect_error)
@@ -263,17 +268,19 @@ class PebbleHandler(ops.charm.Object):
# Verify alive checks if ready checks are missing # Verify alive checks if ready checks are missing
if not checks: if not checks:
checks = container.get_checks( checks = container.get_checks(level=ops.pebble.CheckLevel.ALIVE)
level=ops.pebble.CheckLevel.ALIVE)
for name, check in checks.items(): for name, check in checks.items():
if check.status != ops.pebble.CheckStatus.UP: if check.status != ops.pebble.CheckStatus.UP:
failed.append(name) failed.append(name)
if failed: if failed:
self.status.set(BlockedStatus('healthcheck{} failed: {}'.format( self.status.set(
's' if len(failed) > 1 else '', BlockedStatus(
', '.join(failed) "healthcheck{} failed: {}".format(
))) "s" if len(failed) > 1 else "", ", ".join(failed)
)
)
)
return return
self.status.set(ActiveStatus("")) self.status.set(ActiveStatus(""))
@@ -288,9 +295,10 @@ class PebbleHandler(ops.charm.Object):
for service_name, service in services.items(): for service_name, service in services.items():
if service.is_running() and restart: if service.is_running() and restart:
logger.debug( logger.debug(
f'Stopping {service_name} in {self.container_name}') f"Stopping {service_name} in {self.container_name}"
)
container.stop(service_name) container.stop(service_name)
logger.debug(f'Starting {service_name} in {self.container_name}') logger.debug(f"Starting {service_name} in {self.container_name}")
container.start(service_name) container.start(service_name)
@@ -318,14 +326,15 @@ class ServicePebbleHandler(PebbleHandler):
""" """
container = self.charm.unit.get_container(self.container_name) container = self.charm.unit.get_container(self.container_name)
if not container: if not container:
logger.debug(f'{self.container_name} container is not ready. ' logger.debug(
'Cannot start service.') f"{self.container_name} container is not ready. "
"Cannot start service."
)
return return
if self.service_name not in container.get_services().keys(): if self.service_name not in container.get_services().keys():
container.add_layer( container.add_layer(
self.service_name, self.service_name, self.get_layer(), combine=True
self.get_layer(), )
combine=True)
self._start_all(restart=restart) self._start_all(restart=restart)
@@ -369,9 +378,8 @@ class WSGIPebbleHandler(PebbleHandler):
return return
if self.wsgi_service_name not in container.get_services().keys(): if self.wsgi_service_name not in container.get_services().keys():
container.add_layer( container.add_layer(
self.service_name, self.service_name, self.get_layer(), combine=True
self.get_layer(), )
combine=True)
self._start_all(restart=restart) self._start_all(restart=restart)
def start_service(self) -> None: def start_service(self) -> None:
@@ -409,16 +417,12 @@ class WSGIPebbleHandler(PebbleHandler):
"period": "10s", "period": "10s",
"timeout": "3s", "timeout": "3s",
"threshold": 3, "threshold": 3,
"exec": { "exec": {"command": "service apache2 status"},
"command": "service apache2 status"
}
}, },
"online": { "online": {
"override": "replace", "override": "replace",
"level": "ready", "level": "ready",
"http": { "http": {"url": self.charm.healthcheck_http_url},
"url": self.charm.healthcheck_http_url
}
}, },
} }
} }
@@ -429,13 +433,13 @@ class WSGIPebbleHandler(PebbleHandler):
files_changed = self.write_config(context) files_changed = self.write_config(context)
try: try:
process = container.exec( process = container.exec(
['a2ensite', self.wsgi_service_name], ["a2ensite", self.wsgi_service_name], timeout=5 * 60
timeout=5*60) )
out, warnings = process.wait_output() out, warnings = process.wait_output()
if warnings: if warnings:
for line in warnings.splitlines(): for line in warnings.splitlines():
logger.warning('a2ensite warn: %s', line.strip()) logger.warning("a2ensite warn: %s", line.strip())
logging.debug(f'Output from a2ensite: \n{out}') logging.debug(f"Output from a2ensite: \n{out}")
except ops.pebble.ExecError: except ops.pebble.ExecError:
logger.exception( logger.exception(
f"Failed to enable {self.wsgi_service_name} site in apache" f"Failed to enable {self.wsgi_service_name} site in apache"
@@ -459,7 +463,5 @@ class WSGIPebbleHandler(PebbleHandler):
) -> List[sunbeam_core.ContainerConfigFile]: ) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configs for WSGI service.""" """Container configs for WSGI service."""
return [ return [
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(self.wsgi_conf, "root", "root")
self.wsgi_conf, "root", "root"
)
] ]

View File

@@ -15,17 +15,29 @@
"""Collection of core components.""" """Collection of core components."""
import collections import collections
from typing import Generator, List, TYPE_CHECKING, Tuple, Union from typing import (
TYPE_CHECKING,
Generator,
List,
Tuple,
Union,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from ops_sunbeam.charm import OSBaseOperatorCharm from ops_sunbeam.charm import (
from ops_sunbeam.config_contexts import ConfigContext OSBaseOperatorCharm,
from ops_sunbeam.relation_handlers import RelationHandler )
from ops_sunbeam.config_contexts import (
ConfigContext,
)
from ops_sunbeam.relation_handlers import (
RelationHandler,
)
ContainerConfigFile = collections.namedtuple( ContainerConfigFile = collections.namedtuple(
"ContainerConfigFile", "ContainerConfigFile",
["path", "user", "group", "permissions"], ["path", "user", "group", "permissions"],
defaults=(None,) defaults=(None,),
) )
@@ -61,7 +73,7 @@ class OPSCharmContexts:
def add_config_context( def add_config_context(
self, config_adapter: "ConfigContext", namespace: str self, config_adapter: "ConfigContext", namespace: str
) -> None: ) -> None:
"""Add add config adapater to context.""" """Add add config adapter to context."""
self.namespaces.append(namespace) self.namespaces.append(namespace)
setattr(self, namespace, config_adapter) setattr(self, namespace, config_adapter)

View File

@@ -1,4 +1,4 @@
# Copyright 2021, Canonical Ltd. # Copyright 2021 Canonical Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -15,21 +15,27 @@
"""Module to handle errors and bailing out of an event/hook.""" """Module to handle errors and bailing out of an event/hook."""
import logging import logging
from contextlib import contextmanager from contextlib import (
contextmanager,
)
from ops.charm import CharmBase from ops.charm import (
from ops.model import BlockedStatus CharmBase,
)
from ops.model import (
BlockedStatus,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuardException(Exception): class GuardExceptionError(Exception):
"""GuardException.""" """GuardException."""
pass pass
class BlockedException(Exception): class BlockedExceptionError(Exception):
"""Charm is blocked.""" """Charm is blocked."""
pass pass
@@ -41,7 +47,7 @@ def guard(
section: str, section: str,
handle_exception: bool = True, handle_exception: bool = True,
log_traceback: bool = True, log_traceback: bool = True,
**__ **__,
) -> None: ) -> None:
"""Context manager to handle errors and bailing out of an event/hook. """Context manager to handle errors and bailing out of an event/hook.
@@ -62,13 +68,13 @@ def guard(
try: try:
yield yield
logging.info("Completed guarded section fully: '%s'", section) logging.info("Completed guarded section fully: '%s'", section)
except GuardException as e: except GuardExceptionError as e:
logger.info( logger.info(
"Guarded Section: Early exit from '%s' due to '%s'.", "Guarded Section: Early exit from '%s' due to '%s'.",
section, section,
str(e), str(e),
) )
except BlockedException as e: except BlockedExceptionError as e:
logger.warning( logger.warning(
"Charm is blocked in section '%s' due to '%s'", section, str(e) "Charm is blocked in section '%s' due to '%s'", section, str(e)
) )
@@ -77,14 +83,16 @@ def guard(
# something else went wrong # something else went wrong
if handle_exception: if handle_exception:
logging.error( logging.error(
"Exception raised in secion '%s': %s", section, str(e) "Exception raised in section '%s': %s", section, str(e)
) )
if log_traceback: if log_traceback:
import traceback import traceback
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
charm.status.set(BlockedStatus( charm.status.set(
"Error in charm (see logs): {}".format(str(e)) BlockedStatus(
)) "Error in charm (see logs): {}".format(str(e))
)
)
return return
raise raise

View File

@@ -15,15 +15,20 @@
"""Common interfaces not charm specific.""" """Common interfaces not charm specific."""
import logging import logging
from typing import Dict, List, Optional from typing import (
Dict,
List,
Optional,
)
import ops.model import ops.model
from ops.framework import (
from ops.framework import EventBase EventBase,
from ops.framework import EventSource EventSource,
from ops.framework import Object Object,
from ops.framework import ObjectEvents ObjectEvents,
from ops.framework import StoredState StoredState,
)
class PeersRelationCreatedEvent(EventBase): class PeersRelationCreatedEvent(EventBase):

View File

@@ -14,11 +14,13 @@
"""Base classes for defining an OVN charm using the Operator framework.""" """Base classes for defining an OVN charm using the Operator framework."""
from typing import List from typing import (
List,
)
from . import relation_handlers as ovn_relation_handlers
from .. import relation_handlers as sunbeam_rhandlers
from .. import charm as sunbeam_charm from .. import charm as sunbeam_charm
from .. import relation_handlers as sunbeam_rhandlers
from . import relation_handlers as ovn_relation_handlers
class OSBaseOVNOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): class OSBaseOVNOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):

View File

@@ -28,7 +28,8 @@ class OVNDBConfigContext(sunbeam_ccontexts.ConfigContext):
def context(self) -> dict: def context(self) -> dict:
"""Context for OVN certs and leadership.""" """Context for OVN certs and leadership."""
return { return {
'is_charm_leader': self.charm.unit.is_leader(), "is_charm_leader": self.charm.unit.is_leader(),
'ovn_key': '/etc/ovn/key_host', "ovn_key": "/etc/ovn/key_host",
'ovn_cert': '/etc/ovn/cert_host', "ovn_cert": "/etc/ovn/cert_host",
'ovn_ca_cert': '/etc/ovn/ovn-central.crt'} "ovn_ca_cert": "/etc/ovn/ovn-central.crt",
}

View File

@@ -14,11 +14,13 @@
"""Base classes for defining OVN Pebble handlers.""" """Base classes for defining OVN Pebble handlers."""
from typing import (
List,
)
from .. import container_handlers as sunbeam_chandlers from .. import container_handlers as sunbeam_chandlers
from .. import core as sunbeam_core from .. import core as sunbeam_core
from typing import List
class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Common class for OVN services.""" """Common class for OVN services."""
@@ -47,7 +49,7 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
@property @property
def service_description(self) -> str: def service_description(self) -> str:
"""Return a short decription of service e.g. OVN Southbound DB.""" """Return a short description of service e.g. OVN Southbound DB."""
raise NotImplementedError raise NotImplementedError
def get_layer(self) -> dict: def get_layer(self) -> dict:
@@ -58,8 +60,9 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
""" """
return { return {
"summary": f"{self.service_description} service", "summary": f"{self.service_description} service",
"description": ("Pebble config layer for " "description": (
f"{self.service_description}"), "Pebble config layer for " f"{self.service_description}"
),
"services": { "services": {
self.service_name: { self.service_name: {
"override": "replace", "override": "replace",
@@ -81,9 +84,7 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"online": { "online": {
"override": "replace", "override": "replace",
"level": "ready", "level": "ready",
"exec": { "exec": {"command": f"{self.status_command}"},
"command": f"{self.status_command}"
}
}, },
} }
} }
@@ -92,41 +93,27 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
def directories(self) -> List[sunbeam_chandlers.ContainerDir]: def directories(self) -> List[sunbeam_chandlers.ContainerDir]:
"""Directories to creete in container.""" """Directories to creete in container."""
return [ return [
sunbeam_chandlers.ContainerDir( sunbeam_chandlers.ContainerDir("/etc/ovn", "root", "root"),
'/etc/ovn', sunbeam_chandlers.ContainerDir("/run/ovn", "root", "root"),
'root', sunbeam_chandlers.ContainerDir("/var/lib/ovn", "root", "root"),
'root'), sunbeam_chandlers.ContainerDir("/var/log/ovn", "root", "root"),
sunbeam_chandlers.ContainerDir( ]
'/run/ovn',
'root',
'root'),
sunbeam_chandlers.ContainerDir(
'/var/lib/ovn',
'root',
'root'),
sunbeam_chandlers.ContainerDir(
'/var/log/ovn',
'root',
'root')]
def default_container_configs( def default_container_configs(
self self,
) -> List[sunbeam_core.ContainerConfigFile]: ) -> List[sunbeam_core.ContainerConfigFile]:
"""Files to render into containers.""" """Files to render into containers."""
return [ return [
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
self.wrapper_script, self.wrapper_script, "root", "root"
'root', ),
'root'),
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
'/etc/ovn/key_host', "/etc/ovn/key_host", "root", "root"
'root', ),
'root'),
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
'/etc/ovn/cert_host', "/etc/ovn/cert_host", "root", "root"
'root', ),
'root'),
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
'/etc/ovn/ovn-central.crt', "/etc/ovn/ovn-central.crt", "root", "root"
'root', ),
'root')] ]

View File

@@ -16,10 +16,14 @@
import ipaddress import ipaddress
import itertools import itertools
import socket
import logging import logging
import socket
from typing import Callable, Dict, Iterator, List from typing import (
Callable,
Dict,
Iterator,
List,
)
import ops.charm import ops.charm
import ops.framework import ops.framework
@@ -29,7 +33,7 @@ from .. import relation_handlers as sunbeam_rhandlers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OVNRelationUtils(): class OVNRelationUtils:
"""Common utilities for processing OVN relations.""" """Common utilities for processing OVN relations."""
DB_NB_PORT = 6641 DB_NB_PORT = 6641
@@ -49,9 +53,9 @@ class OVNRelationUtils():
""" """
ipaddr = ipaddress.ip_address(addr) ipaddr = ipaddress.ip_address(addr)
if isinstance(ipaddr, ipaddress.IPv6Address): if isinstance(ipaddr, ipaddress.IPv6Address):
fmt = '[{}]' fmt = "[{}]"
else: else:
fmt = '{}' fmt = "{}"
return fmt.format(ipaddr) return fmt.format(ipaddr)
def _remote_addrs(self, key: str) -> Iterator[str]: def _remote_addrs(self, key: str) -> Iterator[str]:
@@ -87,7 +91,7 @@ class OVNRelationUtils():
:returns: hostnames bound to remote endpoints. :returns: hostnames bound to remote endpoints.
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return self._remote_hostnames('bound-hostname') return self._remote_hostnames("bound-hostname")
@property @property
def cluster_remote_addrs(self) -> Iterator[str]: def cluster_remote_addrs(self) -> Iterator[str]:
@@ -96,13 +100,11 @@ class OVNRelationUtils():
:returns: addresses bound to remote endpoints. :returns: addresses bound to remote endpoints.
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return self._remote_addrs('bound-address') return self._remote_addrs("bound-address")
def db_connection_strs( def db_connection_strs(
self, self, hostnames: List[str], port: int, proto: str = "ssl"
hostnames: List[str], ) -> Iterator[str]:
port: int,
proto: str = 'ssl') -> Iterator[str]:
"""Provide connection strings. """Provide connection strings.
:param hostnames: List of hostnames to include in conn strs :param hostnames: List of hostnames to include in conn strs
@@ -115,7 +117,7 @@ class OVNRelationUtils():
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
for hostname in hostnames: for hostname in hostnames:
yield ':'.join((proto, str(hostname), str(port))) yield ":".join((proto, str(hostname), str(port)))
@property @property
def db_nb_port(self) -> int: def db_nb_port(self) -> int:
@@ -173,8 +175,9 @@ class OVNRelationUtils():
:returns: OVN Northbound OVSDB connection strings. :returns: OVN Northbound OVSDB connection strings.
:rtpye: Iterator[str] :rtpye: Iterator[str]
""" """
return self.db_connection_strs(self.cluster_remote_addrs, return self.db_connection_strs(
self.db_nb_port) self.cluster_remote_addrs, self.db_nb_port
)
@property @property
def db_sb_connection_strs(self) -> Iterator[str]: def db_sb_connection_strs(self) -> Iterator[str]:
@@ -183,8 +186,9 @@ class OVNRelationUtils():
:returns: OVN Southbound OVSDB connection strings. :returns: OVN Southbound OVSDB connection strings.
:rtpye: Iterator[str] :rtpye: Iterator[str]
""" """
return self.db_connection_strs(self.cluster_remote_addrs, return self.db_connection_strs(
self.db_sb_port) self.cluster_remote_addrs, self.db_sb_port
)
@property @property
def db_nb_connection_hostname_strs(self) -> Iterator[str]: def db_nb_connection_hostname_strs(self) -> Iterator[str]:
@@ -193,8 +197,9 @@ class OVNRelationUtils():
:returns: OVN Northbound OVSDB connection strings. :returns: OVN Northbound OVSDB connection strings.
:rtpye: Iterator[str] :rtpye: Iterator[str]
""" """
return self.db_connection_strs(self.cluster_remote_hostnames, return self.db_connection_strs(
self.db_nb_port) self.cluster_remote_hostnames, self.db_nb_port
)
@property @property
def db_sb_connection_hostname_strs(self) -> Iterator[str]: def db_sb_connection_hostname_strs(self) -> Iterator[str]:
@@ -203,8 +208,9 @@ class OVNRelationUtils():
:returns: OVN Southbound OVSDB connection strings. :returns: OVN Southbound OVSDB connection strings.
:rtpye: Iterator[str] :rtpye: Iterator[str]
""" """
return self.db_connection_strs(self.cluster_remote_hostnames, return self.db_connection_strs(
self.db_sb_port) self.cluster_remote_hostnames, self.db_sb_port
)
@property @property
def cluster_local_addr(self) -> ipaddress.IPv4Address: def cluster_local_addr(self) -> ipaddress.IPv4Address:
@@ -237,13 +243,12 @@ class OVNRelationUtils():
return addr return addr
class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler, class OVNDBClusterPeerHandler(
OVNRelationUtils): sunbeam_rhandlers.BasePeerHandler, OVNRelationUtils
):
"""Handle OVN peer relation.""" """Handle OVN peer relation."""
def publish_cluster_local_hostname( def publish_cluster_local_hostname(self, hostname: str = None) -> Dict:
self,
hostname: str = None) -> Dict:
"""Announce hostname on relation. """Announce hostname on relation.
This will be used by our peers and clients to build a connection This will be used by our peers and clients to build a connection
@@ -254,7 +259,7 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
""" """
_hostname = hostname or self.cluster_local_hostname _hostname = hostname or self.cluster_local_hostname
if _hostname: if _hostname:
self.interface.set_unit_data({'bound-hostname': str(_hostname)}) self.interface.set_unit_data({"bound-hostname": str(_hostname)})
def expected_peers_available(self) -> bool: def expected_peers_available(self) -> bool:
"""Whether expected peers have joined and published data on peer rel. """Whether expected peers have joined and published data on peer rel.
@@ -272,17 +277,20 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
if len(joined_units) < expected_remote_units: if len(joined_units) < expected_remote_units:
logging.debug( logging.debug(
f"Expected {expected_remote_units} but only {joined_units} " f"Expected {expected_remote_units} but only {joined_units} "
"have joined so far") "have joined so far"
)
return False return False
hostnames = self.interface.get_all_unit_values('bound-hostname') hostnames = self.interface.get_all_unit_values("bound-hostname")
if all(hostnames) < expected_remote_units: if all(hostnames) < expected_remote_units:
logging.debug( logging.debug(
"Not all units have published a bound-hostname. Current " "Not all units have published a bound-hostname. Current "
f"hostname list: {hostnames}") f"hostname list: {hostnames}"
)
return False return False
else: else:
logging.debug( logging.debug(
f"All expected peers are present. Hostnames: {hostnames}") f"All expected peers are present. Hostnames: {hostnames}"
)
return True return True
@property @property
@@ -296,10 +304,13 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return itertools.chain( return itertools.chain(
self.db_connection_strs((self.cluster_local_hostname,), self.db_connection_strs(
self.db_nb_port), (self.cluster_local_hostname,), self.db_nb_port
self.db_connection_strs(self.cluster_remote_hostnames, ),
self.db_nb_port)) self.db_connection_strs(
self.cluster_remote_hostnames, self.db_nb_port
),
)
@property @property
def db_nb_cluster_connection_strs(self) -> Iterator[str]: def db_nb_cluster_connection_strs(self) -> Iterator[str]:
@@ -312,10 +323,13 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return itertools.chain( return itertools.chain(
self.db_connection_strs((self.cluster_local_hostname,), self.db_connection_strs(
self.db_nb_cluster_port), (self.cluster_local_hostname,), self.db_nb_cluster_port
self.db_connection_strs(self.cluster_remote_hostnames, ),
self.db_nb_cluster_port)) self.db_connection_strs(
self.cluster_remote_hostnames, self.db_nb_cluster_port
),
)
@property @property
def db_sb_cluster_connection_strs(self) -> Iterator[str]: def db_sb_cluster_connection_strs(self) -> Iterator[str]:
@@ -328,10 +342,13 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return itertools.chain( return itertools.chain(
self.db_connection_strs((self.cluster_local_hostname,), self.db_connection_strs(
self.db_sb_cluster_port), (self.cluster_local_hostname,), self.db_sb_cluster_port
self.db_connection_strs(self.cluster_remote_hostnames, ),
self.db_sb_cluster_port)) self.db_connection_strs(
self.cluster_remote_hostnames, self.db_sb_cluster_port
),
)
@property @property
def db_sb_connection_strs(self) -> Iterator[str]: def db_sb_connection_strs(self) -> Iterator[str]:
@@ -346,35 +363,41 @@ class OVNDBClusterPeerHandler(sunbeam_rhandlers.BasePeerHandler,
:rtype: Iterator[str] :rtype: Iterator[str]
""" """
return itertools.chain( return itertools.chain(
self.db_connection_strs((self.cluster_local_hostname,), self.db_connection_strs(
self.db_sb_admin_port), (self.cluster_local_hostname,), self.db_sb_admin_port
self.db_connection_strs(self.cluster_remote_hostnames, ),
self.db_sb_admin_port)) self.db_connection_strs(
self.cluster_remote_hostnames, self.db_sb_admin_port
),
)
def _on_peers_relation_joined( def _on_peers_relation_joined(
self, event: ops.framework.EventBase) -> None: self, event: ops.framework.EventBase
) -> None:
"""Process peer joined event.""" """Process peer joined event."""
self.publish_cluster_local_hostname() self.publish_cluster_local_hostname()
def context(self) -> dict: def context(self) -> dict:
"""Context from relation data.""" """Context from relation data."""
ctxt = super().context() ctxt = super().context()
ctxt.update({ ctxt.update(
'cluster_local_hostname': self.cluster_local_hostname, {
'cluster_remote_hostnames': self.cluster_remote_hostnames, "cluster_local_hostname": self.cluster_local_hostname,
'db_nb_cluster_connection_strs': "cluster_remote_hostnames": self.cluster_remote_hostnames,
self.db_nb_cluster_connection_strs, "db_nb_cluster_connection_strs": self.db_nb_cluster_connection_strs,
'db_sb_cluster_connection_strs': "db_sb_cluster_connection_strs": self.db_sb_cluster_connection_strs,
self.db_sb_cluster_connection_strs, "db_sb_cluster_port": self.db_sb_cluster_port,
'db_sb_cluster_port': self.db_sb_cluster_port, "db_nb_cluster_port": self.db_nb_cluster_port,
'db_nb_cluster_port': self.db_nb_cluster_port, "db_nb_connection_strs": list(self.db_nb_connection_strs),
'db_nb_connection_strs': list(self.db_nb_connection_strs), "db_sb_connection_strs": list(self.db_sb_connection_strs),
'db_sb_connection_strs': list(self.db_sb_connection_strs)}) }
)
return ctxt return ctxt
class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler, class OVSDBCMSProvidesHandler(
OVNRelationUtils): sunbeam_rhandlers.RelationHandler, OVNRelationUtils
):
"""Handle provides side of ovsdb-cms.""" """Handle provides side of ovsdb-cms."""
def __init__( def __init__(
@@ -394,13 +417,14 @@ class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler,
# has this relation. # has this relation.
logger.debug("Setting up ovs-cms provides event handler") logger.debug("Setting up ovs-cms provides event handler")
import charms.ovn_central_k8s.v0.ovsdb as ovsdb import charms.ovn_central_k8s.v0.ovsdb as ovsdb
ovsdb_svc = ovsdb.OVSDBCMSProvides( ovsdb_svc = ovsdb.OVSDBCMSProvides(
self.charm, self.charm,
self.relation_name, self.relation_name,
) )
self.framework.observe( self.framework.observe(
ovsdb_svc.on.ready, ovsdb_svc.on.ready, self._on_ovsdb_service_ready
self._on_ovsdb_service_ready) )
return ovsdb_svc return ovsdb_svc
def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None: def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None:
@@ -409,10 +433,12 @@ class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler,
def _update_address_data(self) -> None: def _update_address_data(self) -> None:
"""Update hostname and IP address data on all relations.""" """Update hostname and IP address data on all relations."""
self.interface.set_unit_data({ self.interface.set_unit_data(
'bound-hostname': str(self.cluster_local_hostname), {
'bound-address': str(self.cluster_local_addr), "bound-hostname": str(self.cluster_local_hostname),
}) "bound-address": str(self.cluster_local_addr),
}
)
@property @property
def ready(self) -> bool: def ready(self) -> bool:
@@ -420,8 +446,9 @@ class OVSDBCMSProvidesHandler(sunbeam_rhandlers.RelationHandler,
return True return True
class OVSDBCMSRequiresHandler(sunbeam_rhandlers.RelationHandler, class OVSDBCMSRequiresHandler(
OVNRelationUtils): sunbeam_rhandlers.RelationHandler, OVNRelationUtils
):
"""Handle provides side of ovsdb-cms.""" """Handle provides side of ovsdb-cms."""
def __init__( def __init__(
@@ -440,13 +467,14 @@ class OVSDBCMSRequiresHandler(sunbeam_rhandlers.RelationHandler,
# has this relation. # has this relation.
logger.debug("Setting up ovs-cms requires event handler") logger.debug("Setting up ovs-cms requires event handler")
import charms.ovn_central_k8s.v0.ovsdb as ovsdb import charms.ovn_central_k8s.v0.ovsdb as ovsdb
ovsdb_svc = ovsdb.OVSDBCMSRequires( ovsdb_svc = ovsdb.OVSDBCMSRequires(
self.charm, self.charm,
self.relation_name, self.relation_name,
) )
self.framework.observe( self.framework.observe(
ovsdb_svc.on.ready, ovsdb_svc.on.ready, self._on_ovsdb_service_ready
self._on_ovsdb_service_ready) )
return ovsdb_svc return ovsdb_svc
def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None: def _on_ovsdb_service_ready(self, event: ops.framework.EventBase) -> None:
@@ -461,17 +489,21 @@ class OVSDBCMSRequiresHandler(sunbeam_rhandlers.RelationHandler,
def context(self) -> dict: def context(self) -> dict:
"""Context from relation data.""" """Context from relation data."""
ctxt = super().context() ctxt = super().context()
ctxt.update({ ctxt.update(
'local_hostname': self.cluster_local_hostname, {
'hostnames': self.interface.bound_hostnames(), "local_hostname": self.cluster_local_hostname,
'local_address': self.cluster_local_addr, "hostnames": self.interface.bound_hostnames(),
'addresses': self.interface.bound_addresses(), "local_address": self.cluster_local_addr,
'db_sb_connection_strs': ','.join(self.db_sb_connection_strs), "addresses": self.interface.bound_addresses(),
'db_nb_connection_strs': ','.join(self.db_nb_connection_strs), "db_sb_connection_strs": ",".join(self.db_sb_connection_strs),
'db_sb_connection_hostname_strs': "db_nb_connection_strs": ",".join(self.db_nb_connection_strs),
','.join(self.db_sb_connection_hostname_strs), "db_sb_connection_hostname_strs": ",".join(
'db_nb_connection_hostname_strs': self.db_sb_connection_hostname_strs
','.join(self.db_nb_connection_hostname_strs) ),
}) "db_nb_connection_hostname_strs": ",".join(
self.db_nb_connection_hostname_strs
),
}
)
return ctxt return ctxt

View File

@@ -16,21 +16,33 @@
import json import json
import logging import logging
import cryptography.hazmat.primitives.serialization as serialization from typing import (
from typing import Callable, List, Tuple, Optional Callable,
from urllib.parse import urlparse List,
Optional,
Tuple,
)
from urllib.parse import (
urlparse,
)
import cryptography.hazmat.primitives.serialization as serialization
import ops.charm import ops.charm
import ops.framework import ops.framework
from ops.model import BlockedStatus, ActiveStatus, WaitingStatus, UnknownStatus from ops.model import (
ActiveStatus,
BlockedStatus,
UnknownStatus,
WaitingStatus,
)
import ops_sunbeam.interfaces as sunbeam_interfaces
import ops_sunbeam.compound_status as compound_status import ops_sunbeam.compound_status as compound_status
import ops_sunbeam.interfaces as sunbeam_interfaces
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ERASURE_CODED = "erasure-coded" ERASURE_CODED = "erasure-coded"
REPLICATED = "replacated" REPLICATED = "replicated"
class RelationHandler(ops.charm.Object): class RelationHandler(ops.charm.Object):
@@ -42,9 +54,9 @@ class RelationHandler(ops.charm.Object):
1) Registering handlers to process events from the interface. The last 1) Registering handlers to process events from the interface. The last
step of these handlers is to make a callback to a specified method step of these handlers is to make a callback to a specified method
within the charm `callback_f` within the charm `callback_f`
2) Expose a `ready` property so the charm can check a relations readyness 2) Expose a `ready` property so the charm can check a relations readiness
3) A `context` method which returns a dict which pulls together data 3) A `context` method which returns a dict which pulls together data
recieved and sent on an interface. received and sent on an interface.
""" """
def __init__( def __init__(
@@ -59,7 +71,7 @@ class RelationHandler(ops.charm.Object):
charm, charm,
# Ensure we can have multiple instances of a relation handler, # Ensure we can have multiple instances of a relation handler,
# but only one per relation. # but only one per relation.
key=type(self).__name__ + '_' + relation_name key=type(self).__name__ + "_" + relation_name,
) )
self.charm = charm self.charm = charm
self.relation_name = relation_name self.relation_name = relation_name
@@ -71,8 +83,7 @@ class RelationHandler(ops.charm.Object):
self.set_status(status) self.set_status(status)
def set_status(self, status: compound_status.Status) -> None: def set_status(self, status: compound_status.Status) -> None:
""" """Set the status based on current state.
Set the status based on current state.
Will be called once, during construction, Will be called once, during construction,
after everything else is initialised. after everything else is initialised.
@@ -148,37 +159,34 @@ class IngressHandler(RelationHandler):
def setup_event_handler(self) -> ops.charm.Object: def setup_event_handler(self) -> ops.charm.Object:
"""Configure event handlers for an Ingress relation.""" """Configure event handlers for an Ingress relation."""
logger.debug("Setting up ingress event handler") logger.debug("Setting up ingress event handler")
from charms.traefik_k8s.v1.ingress import IngressPerAppRequirer from charms.traefik_k8s.v1.ingress import (
IngressPerAppRequirer,
)
interface = IngressPerAppRequirer( interface = IngressPerAppRequirer(
self.charm, self.charm,
self.relation_name, self.relation_name,
port=self.default_ingress_port, port=self.default_ingress_port,
) )
self.framework.observe( self.framework.observe(interface.on.ready, self._on_ingress_ready)
interface.on.ready, self._on_ingress_ready self.framework.observe(interface.on.revoked, self._on_ingress_revoked)
)
self.framework.observe(
interface.on.revoked, self._on_ingress_revoked
)
return interface return interface
def _on_ingress_ready(self, event) -> None: # noqa: ANN001 def _on_ingress_ready(self, event) -> None: # noqa: ANN001
""" """Handle ingress relation changed events.
Handle ingress relation changed events.
`event` is an instance of `event` is an instance of
`charms.traefik_k8s.v1.ingress.IngressPerAppReadyEvent`. `charms.traefik_k8s.v1.ingress.IngressPerAppReadyEvent`.
""" """
url = self.url url = self.url
logger.debug(f'Received url: {url}') logger.debug(f"Received url: {url}")
if not url: if not url:
return return
self.callback_f(event) self.callback_f(event)
def _on_ingress_revoked(self, event) -> None: # noqa: ANN001 def _on_ingress_revoked(self, event) -> None: # noqa: ANN001
""" """Handle ingress relation revoked event.
Handle ingress relation revoked event.
`event` is an instance of `event` is an instance of
`charms.traefik_k8s.v1.ingress.IngressPerAppRevokedEvent` `charms.traefik_k8s.v1.ingress.IngressPerAppRevokedEvent`
@@ -207,7 +215,7 @@ class IngressHandler(RelationHandler):
"""Context containing ingress data.""" """Context containing ingress data."""
parse_result = urlparse(self.url) parse_result = urlparse(self.url)
return { return {
'ingress_path': parse_result.path, "ingress_path": parse_result.path,
} }
@@ -242,26 +250,29 @@ class DBHandler(RelationHandler):
# with a charm that doesn't want a DBHandler # with a charm that doesn't want a DBHandler
# and doesn't install this database_requires library. # and doesn't install this database_requires library.
from charms.data_platform_libs.v0.database_requires import ( from charms.data_platform_libs.v0.database_requires import (
DatabaseRequires DatabaseRequires,
) )
# Alias is required to events for this db # Alias is required to events for this db
# from trigger handlers for other dbs. # from trigger handlers for other dbs.
# It also must be a valid python identifier. # It also must be a valid python identifier.
alias = self.relation_name.replace("-", "_") alias = self.relation_name.replace("-", "_")
db = DatabaseRequires( db = DatabaseRequires(
self.charm, self.relation_name, self.database_name, self.charm,
relations_aliases=[alias] self.relation_name,
self.database_name,
relations_aliases=[alias],
) )
self.framework.observe( self.framework.observe(
# db.on[f"{alias}_database_created"], # this doesn't work because: # db.on[f"{alias}_database_created"], # this doesn't work because:
# RuntimeError: Framework.observe requires a BoundEvent as # RuntimeError: Framework.observe requires a BoundEvent as
# second parameter, got <ops.framework.PrefixedEvents object ... # second parameter, got <ops.framework.PrefixedEvents object ...
getattr(db.on, f"{alias}_database_created"), getattr(db.on, f"{alias}_database_created"),
self._on_database_updated self._on_database_updated,
) )
self.framework.observe( self.framework.observe(
getattr(db.on, f"{alias}_endpoints_changed"), getattr(db.on, f"{alias}_endpoints_changed"),
self._on_database_updated self._on_database_updated,
) )
# this will be set to self.interface in parent class # this will be set to self.interface in parent class
return db return db
@@ -289,9 +300,9 @@ class DBHandler(RelationHandler):
"""Whether the handler is ready for use.""" """Whether the handler is ready for use."""
data = self.get_relation_data() data = self.get_relation_data()
return bool( return bool(
data.get("endpoints") and data.get("endpoints")
data.get("username") and and data.get("username")
data.get("password") and data.get("password")
) )
def context(self) -> dict: def context(self) -> dict:
@@ -356,6 +367,7 @@ class RabbitMQHandler(RelationHandler):
# Lazy import to ensure this lib is only required if the charm # Lazy import to ensure this lib is only required if the charm
# has this relation. # has this relation.
import charms.rabbitmq_k8s.v0.rabbitmq as sunbeam_rabbitmq import charms.rabbitmq_k8s.v0.rabbitmq as sunbeam_rabbitmq
amqp = sunbeam_rabbitmq.RabbitMQRequires( amqp = sunbeam_rabbitmq.RabbitMQRequires(
self.charm, self.relation_name, self.username, self.vhost self.charm, self.relation_name, self.username, self.vhost
) )
@@ -433,6 +445,7 @@ class IdentityServiceRequiresHandler(RelationHandler):
"""Configure event handlers for an Identity service relation.""" """Configure event handlers for an Identity service relation."""
logger.debug("Setting up Identity Service event handler") logger.debug("Setting up Identity Service event handler")
import charms.keystone_k8s.v0.identity_service as sun_id import charms.keystone_k8s.v0.identity_service as sun_id
id_svc = sun_id.IdentityServiceRequires( id_svc = sun_id.IdentityServiceRequires(
self.charm, self.relation_name, self.service_endpoints, self.region self.charm, self.relation_name, self.service_endpoints, self.region
) )
@@ -486,7 +499,8 @@ class BasePeerHandler(RelationHandler):
return peer_int return peer_int
def _on_peers_relation_joined( def _on_peers_relation_joined(
self, event: ops.framework.EventBase) -> None: self, event: ops.framework.EventBase
) -> None:
"""Process peer joined event.""" """Process peer joined event."""
self.callback_f(event) self.callback_f(event)
@@ -503,8 +517,9 @@ class BasePeerHandler(RelationHandler):
"""Return all app data set on the peer relation.""" """Return all app data set on the peer relation."""
try: try:
_db = { _db = {
k.replace('-', '_'): v k.replace("-", "_"): v
for k, v in self.interface.get_all_app_data().items()} for k, v in self.interface.get_all_app_data().items()
}
return _db return _db
except AttributeError: except AttributeError:
return {} return {}
@@ -563,6 +578,7 @@ class CephClientHandler(RelationHandler):
# Lazy import to ensure this lib is only required if the charm # Lazy import to ensure this lib is only required if the charm
# has this relation. # has this relation.
import interface_ceph_client.ceph_client as ceph_client import interface_ceph_client.ceph_client as ceph_client
ceph = ceph_client.CephClientRequires( ceph = ceph_client.CephClientRequires(
self.charm, self.charm,
self.relation_name, self.relation_name,
@@ -570,9 +586,7 @@ class CephClientHandler(RelationHandler):
self.framework.observe( self.framework.observe(
ceph.on.pools_available, self._on_pools_available ceph.on.pools_available, self._on_pools_available
) )
self.framework.observe( self.framework.observe(ceph.on.broker_available, self.request_pools)
ceph.on.broker_available, self.request_pools
)
return ceph return ceph
def _on_pools_available(self, event: ops.framework.EventBase) -> None: def _on_pools_available(self, event: ops.framework.EventBase) -> None:
@@ -582,8 +596,7 @@ class CephClientHandler(RelationHandler):
self.callback_f(event) self.callback_f(event)
def request_pools(self, event: ops.framework.EventBase) -> None: def request_pools(self, event: ops.framework.EventBase) -> None:
""" """Request Ceph pool creation when interface broker is ready.
Request Ceph pool creation when interface broker is ready.
The default handler will automatically request erasure-coded The default handler will automatically request erasure-coded
or replicated pools depending on the configuration of the or replicated pools depending on the configuration of the
@@ -595,9 +608,9 @@ class CephClientHandler(RelationHandler):
""" """
config = self.model.config.get config = self.model.config.get
data_pool_name = ( data_pool_name = (
config("rbd-pool-name") or config("rbd-pool-name")
config("rbd-pool") or or config("rbd-pool")
self.charm.app.name or self.charm.app.name
) )
metadata_pool_name = ( metadata_pool_name = (
config("ec-rbd-metadata-pool") or f"{self.charm.app.name}-metadata" config("ec-rbd-metadata-pool") or f"{self.charm.app.name}-metadata"
@@ -628,7 +641,7 @@ class CephClientHandler(RelationHandler):
# but is in effect driven by the number of rbd's rather than # but is in effect driven by the number of rbd's rather than
# their size - so it can be very lightweight. # their size - so it can be very lightweight.
metadata_weight = weight * 0.01 metadata_weight = weight * 0.01
# Resize data pool weight to accomodate metadata weight # Resize data pool weight to accommodate metadata weight
weight = weight - metadata_weight weight = weight - metadata_weight
# Create erasure profile # Create erasure profile
self.interface.create_erasure_profile( self.interface.create_erasure_profile(
@@ -662,7 +675,9 @@ class CephClientHandler(RelationHandler):
) )
else: else:
self.interface.create_replicated_pool( self.interface.create_replicated_pool(
name=data_pool_name, replicas=replicas, weight=weight, name=data_pool_name,
replicas=replicas,
weight=weight,
app_name=self.app_name, app_name=self.app_name,
) )
@@ -674,18 +689,16 @@ class CephClientHandler(RelationHandler):
@property @property
def key(self) -> str: def key(self) -> str:
"""Retrieve the cephx key provided for the application.""" """Retrieve the cephx key provided for the application."""
return self.interface.get_relation_data().get('key') return self.interface.get_relation_data().get("key")
def context(self) -> dict: def context(self) -> dict:
"""Context containing Ceph connection data.""" """Context containing Ceph connection data."""
ctxt = super().context() ctxt = super().context()
data = self.interface.get_relation_data() data = self.interface.get_relation_data()
ctxt['mon_hosts'] = ",".join( ctxt["mon_hosts"] = ",".join(sorted(data.get("mon_hosts")))
sorted(data.get("mon_hosts")) ctxt["auth"] = data.get("auth")
) ctxt["key"] = data.get("key")
ctxt['auth'] = data.get('auth') ctxt["rbd_features"] = None
ctxt['key'] = data.get("key")
ctxt['rbd_features'] = None
return ctxt return ctxt
@@ -704,6 +717,7 @@ class CertificatesHandler(RelationHandler):
# Lazy import to ensure this lib is only required if the charm # Lazy import to ensure this lib is only required if the charm
# has this relation. # has this relation.
import interface_tls_certificates.ca_client as ca_client import interface_tls_certificates.ca_client as ca_client
self.ca_client = ca_client self.ca_client = ca_client
self.sans = sans self.sans = sans
super().__init__(charm, relation_name, callback_f, mandatory) super().__init__(charm, relation_name, callback_f, mandatory)
@@ -715,20 +729,18 @@ class CertificatesHandler(RelationHandler):
self.charm, self.charm,
self.relation_name, self.relation_name,
) )
self.framework.observe(certs.on.ca_available, self._request_certs)
self.framework.observe( self.framework.observe(
certs.on.ca_available, certs.on.tls_server_config_ready, self._certs_ready
self._request_certs) )
self.framework.observe(
certs.on.tls_server_config_ready,
self._certs_ready)
return certs return certs
def _request_certs(self, event: ops.framework.EventBase) -> None: def _request_certs(self, event: ops.framework.EventBase) -> None:
"""Request Certificates.""" """Request Certificates."""
logger.debug(f"Requesting cert for {self.sans}") logger.debug(f"Requesting cert for {self.sans}")
self.interface.request_server_certificate( self.interface.request_server_certificate(
self.model.unit.name.replace('/', '-'), self.model.unit.name.replace("/", "-"), self.sans
self.sans) )
self.callback_f(event) self.callback_f(event)
def _certs_ready(self, event: ops.framework.EventBase) -> None: def _certs_ready(self, event: ops.framework.EventBase) -> None:
@@ -745,9 +757,11 @@ class CertificatesHandler(RelationHandler):
key = self.interface.server_key.private_bytes( key = self.interface.server_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()) encryption_algorithm=serialization.NoEncryption(),
)
cert = self.interface.server_certificate.public_bytes( cert = self.interface.server_certificate.public_bytes(
encoding=serialization.Encoding.PEM) encoding=serialization.Encoding.PEM
)
try: try:
root_ca_chain = self.interface.root_ca_chain.public_bytes( root_ca_chain = self.interface.root_ca_chain.public_bytes(
encoding=serialization.Encoding.PEM encoding=serialization.Encoding.PEM
@@ -762,12 +776,15 @@ class CertificatesHandler(RelationHandler):
root_ca_chain = bytes() root_ca_chain = bytes()
ca_cert = ( ca_cert = (
self.interface.ca_certificate.public_bytes( self.interface.ca_certificate.public_bytes(
encoding=serialization.Encoding.PEM) + encoding=serialization.Encoding.PEM
root_ca_chain) )
+ root_ca_chain
)
ctxt = { ctxt = {
'key': key.decode(), "key": key.decode(),
'cert': cert.decode(), "cert": cert.decode(),
'ca_cert': ca_cert.decode()} "ca_cert": ca_cert.decode(),
}
return ctxt return ctxt
@@ -798,15 +815,15 @@ class CloudCredentialsRequiresHandler(RelationHandler):
def setup_event_handler(self) -> ops.charm.Object: def setup_event_handler(self) -> ops.charm.Object:
"""Configure event handlers for cloud-credentials relation.""" """Configure event handlers for cloud-credentials relation."""
import charms.keystone_k8s.v0.cloud_credentials as \ import charms.keystone_k8s.v0.cloud_credentials as cloud_credentials
cloud_credentials
logger.debug('Setting up the cloud-credentials event handler') logger.debug("Setting up the cloud-credentials event handler")
credentials_service = cloud_credentials.CloudCredentialsRequires( credentials_service = cloud_credentials.CloudCredentialsRequires(
self.charm, self.relation_name, self.charm,
self.relation_name,
) )
self.framework.observe( self.framework.observe(
credentials_service.on.ready, credentials_service.on.ready, self._credentials_ready
self._credentials_ready
) )
return credentials_service return credentials_service

View File

@@ -1,4 +1,4 @@
# Copyright 2021, Canonical Ltd. # Copyright 2021 Canonical Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -16,8 +16,13 @@
import logging import logging
import os import os
from pathlib import Path from pathlib import (
from typing import List, TYPE_CHECKING Path,
)
from typing import (
TYPE_CHECKING,
List,
)
import ops.pebble import ops.pebble
@@ -25,15 +30,17 @@ if TYPE_CHECKING:
import ops_sunbeam.core as sunbeam_core import ops_sunbeam.core as sunbeam_core
import ops.model import ops.model
from charmhelpers.contrib.openstack.templating import get_loader
import jinja2 import jinja2
from charmhelpers.contrib.openstack.templating import (
get_loader,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_container( def get_container(
containers: List['ops.model.Container'], name: str containers: List["ops.model.Container"], name: str
) -> 'ops.model.Container': ) -> "ops.model.Container":
"""Search for container with given name inlist of containers.""" """Search for container with given name inlist of containers."""
container = None container = None
for c in containers: for c in containers:
@@ -43,11 +50,11 @@ def get_container(
def sidecar_config_render( def sidecar_config_render(
container: 'ops.model.Container', container: "ops.model.Container",
config: 'sunbeam_core.ContainerConfigFile', config: "sunbeam_core.ContainerConfigFile",
template_dir: str, template_dir: str,
openstack_release: str, openstack_release: str,
context: 'sunbeam_core.OPSCharmContexts', context: "sunbeam_core.OPSCharmContexts",
) -> bool: ) -> bool:
"""Render templates inside containers. """Render templates inside containers.
@@ -56,7 +63,7 @@ def sidecar_config_render(
""" """
file_updated = False file_updated = False
try: try:
original_contents = (container.pull(config.path).read()) original_contents = container.pull(config.path).read()
except (ops.pebble.PathError, FileNotFoundError): except (ops.pebble.PathError, FileNotFoundError):
original_contents = None original_contents = None
loader = get_loader(template_dir, openstack_release) loader = get_loader(template_dir, openstack_release)
@@ -66,9 +73,7 @@ def sidecar_config_render(
os.path.basename(config.path) + ".j2" os.path.basename(config.path) + ".j2"
) )
except jinja2.exceptions.TemplateNotFound: except jinja2.exceptions.TemplateNotFound:
template = _tmpl_env.get_template( template = _tmpl_env.get_template(os.path.basename(config.path))
os.path.basename(config.path)
)
contents = template.render(context) contents = template.render(context)
if original_contents == contents: if original_contents == contents:
log.debug( log.debug(
@@ -78,7 +83,8 @@ def sidecar_config_render(
kwargs = { kwargs = {
"user": config.user, "user": config.user,
"group": config.group, "group": config.group,
"permissions": config.permissions} "permissions": config.permissions,
}
parent_dir = str(Path(config.path).parent) parent_dir = str(Path(config.path).parent)
if not container.isdir(parent_dir): if not container.isdir(parent_dir):
container.make_dir(parent_dir, make_parents=True) container.make_dir(parent_dir, make_parents=True)

View File

@@ -16,33 +16,40 @@
"""Module containing shared code to be used in a charms units tests.""" """Module containing shared code to be used in a charms units tests."""
import collections
import inspect import inspect
import json import json
import ops
import os import os
import pathlib import pathlib
import sys import sys
import typing import typing
import unittest import unittest
import collections from typing import (
from typing import List List,
)
from mock import MagicMock, Mock, patch import ops
from mock import (
MagicMock,
Mock,
patch,
)
sys.path.append("lib") # noqa sys.path.append("lib") # noqa
sys.path.append("src") # noqa sys.path.append("src") # noqa
from ops import framework, model from ops import (
framework,
model,
)
from ops.testing import ( from ops.testing import (
SIMULATE_CAN_CONNECT,
Harness, Harness,
_TestingModelBackend, _TestingModelBackend,
_TestingPebbleClient, _TestingPebbleClient,
SIMULATE_CAN_CONNECT,
) )
TEST_CA = """-----BEGIN CERTIFICATE-----
TEST_CA = '''-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUOTGfdiGSlKoiyWskxH1za0Nh7cYwDQYJKoZIhvcNAQEL MIIDADCCAeigAwIBAgIUOTGfdiGSlKoiyWskxH1za0Nh7cYwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIyMDIwNjE4MjYyM1oX BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIyMDIwNjE4MjYyM1oX
DTMzMDEyMDE4MjYyM1owRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENl DTMzMDEyMDE4MjYyM1owRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENl
@@ -60,9 +67,9 @@ zT8PgdjdzBW80l7KAMy4/GzZvvK7MWfkkhwwnY7oXs9F3q28gFIdcYyc9A1SDg/8
8jWI6RP5yBcNS/PgUmVV+Ko1uTHxNsKjOn7QPuUgjMBeW0fpBCHVFxz7rs+orHNF 8jWI6RP5yBcNS/PgUmVV+Ko1uTHxNsKjOn7QPuUgjMBeW0fpBCHVFxz7rs+orHNF
JSWcYpOxivTh+YO8cAxAGlKzrgZDcXQDjGfF34U/v3niDUHO+CAk6Jz3io4Oxh2X JSWcYpOxivTh+YO8cAxAGlKzrgZDcXQDjGfF34U/v3niDUHO+CAk6Jz3io4Oxh2X
GksTPQ== GksTPQ==
-----END CERTIFICATE-----''' -----END CERTIFICATE-----"""
TEST_CHAIN = '''-----BEGIN CERTIFICATE----- TEST_CHAIN = """-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUOTGfdiGSlKoiyWskxH1za0Nh7cYwDQYJKoZIhvcNAQEL MIIDADCCAeigAwIBAgIUOTGfdiGSlKoiyWskxH1za0Nh7cYwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIyMDIwNjE4MjYyM1oX BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIyMDIwNjE4MjYyM1oX
DTMzMDEyMDE4MjYyM1owRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENl DTMzMDEyMDE4MjYyM1owRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENl
@@ -80,9 +87,9 @@ zT8PgdjdzBW80l7KAMy4/GzZvvK7MWfkkhwwnY7oXs9F3q28gFIdcYyc9A1SDg/8
8jWI6RP5yBcNS/PgUmVV+Ko1uTHxNsKjOn7QPuUgjMBeW0fpBCHVFxz7rs+orHNF 8jWI6RP5yBcNS/PgUmVV+Ko1uTHxNsKjOn7QPuUgjMBeW0fpBCHVFxz7rs+orHNF
JSWcYpOxivTh+YO8cAxAGlKzrgZDcXQDjGfF34U/v3niDUHO+CAk6Jz3io4Oxh2X JSWcYpOxivTh+YO8cAxAGlKzrgZDcXQDjGfF34U/v3niDUHO+CAk6Jz3io4Oxh2X
GksTPQ== GksTPQ==
-----END CERTIFICATE-----''' -----END CERTIFICATE-----"""
TEST_SERVER_CERT = '''-----BEGIN CERTIFICATE----- TEST_SERVER_CERT = """-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgIUIRVQ0iFgTDBP+Ju6AlcnxTHywUgwDQYJKoZIhvcNAQEL MIIEEzCCAvugAwIBAgIUIRVQ0iFgTDBP+Ju6AlcnxTHywUgwDQYJKoZIhvcNAQEL
BQAwRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRlIEF1 BQAwRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRlIEF1
dGhvcml0eSAoY2hhcm0tcGtpLWxvY2FsKTAeFw0yMjAyMDcxODI1NTlaFw0yMzAy dGhvcml0eSAoY2hhcm0tcGtpLWxvY2FsKTAeFw0yMjAyMDcxODI1NTlaFw0yMzAy
@@ -105,9 +112,9 @@ wzSbqkarasPFVpPJnFAGqry6y5B3lZ3OrhHJOIwMSOMQfPt2dSsz+HqfrMwxqAek
smciCVWqVwN+uq0yqeH5QuACHlkJSV4o/5SkDcFZFaFHuTRqd6hMpczZIw+o+NRn smciCVWqVwN+uq0yqeH5QuACHlkJSV4o/5SkDcFZFaFHuTRqd6hMpczZIw+o+NRn
OO1YV69oqCCfUE01zlwTF7thZA19xacGS9f8GJO9Ij15MiysZLjxoTfoof/wDdNd OO1YV69oqCCfUE01zlwTF7thZA19xacGS9f8GJO9Ij15MiysZLjxoTfoof/wDdNd
A0Rs/pW3ja1UfTItPdjC4BgWtQh1a7O9NznrW2L6nRCASI0F1FvQ A0Rs/pW3ja1UfTItPdjC4BgWtQh1a7O9NznrW2L6nRCASI0F1FvQ
-----END CERTIFICATE-----''' -----END CERTIFICATE-----"""
TEST_SERVER_KEY = '''-----BEGIN RSA PRIVATE KEY----- TEST_SERVER_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4VYeKjC3o9GZAnbuVBudyd/a5sHnaGZlMJz8zevhGr5nARRR MIIEowIBAAKCAQEA4VYeKjC3o9GZAnbuVBudyd/a5sHnaGZlMJz8zevhGr5nARRR
194bgR8VSB9k1fRbF1Y9WTygBW5aiXPy+KbmaD5DsDpJNkF/2zOQDLG9nKmLbamr 194bgR8VSB9k1fRbF1Y9WTygBW5aiXPy+KbmaD5DsDpJNkF/2zOQDLG9nKmLbamr
AcHFU8l8kAVwkdhYgu3T8QbLksozYPiYavg9KfA51wVxTRuUyLpvSLJkc1q0xwuJ AcHFU8l8kAVwkdhYgu3T8QbLksozYPiYavg9KfA51wVxTRuUyLpvSLJkc1q0xwuJ
@@ -133,7 +140,7 @@ C8l5gTQQnHu3h5Z7HX97GWgn1ql4X1MUr+aP6Mq9CgqzCn8s/CAZeEhOIXVgwFPq
a03odwKBgG454yINXnHPBo9jjcEKwBTaMLH0n25HMJmWaJUnGVmPzrhxHp5xMKZz a03odwKBgG454yINXnHPBo9jjcEKwBTaMLH0n25HMJmWaJUnGVmPzrhxHp5xMKZz
ULTaKTN2gp7E2BuxENtAyplrvLiXXYH3CqT528JgMdMm0al6X3MXo9WqbOg/KNpa ULTaKTN2gp7E2BuxENtAyplrvLiXXYH3CqT528JgMdMm0al6X3MXo9WqbOg/KNpa
4JSyyuZ42yGmYlhMCimlk3kVnDxb8PJLWOFnx6f9/i0RWUqnY0nU 4JSyyuZ42yGmYlhMCimlk3kVnDxb8PJLWOFnx6f9/i0RWUqnY0nU
-----END RSA PRIVATE KEY-----''' -----END RSA PRIVATE KEY-----"""
class ContainerCalls: class ContainerCalls:
@@ -158,17 +165,27 @@ class ContainerCalls:
def started_services(self, container_name: str) -> List: def started_services(self, container_name: str) -> List:
"""Distinct unordered list of services that were started.""" """Distinct unordered list of services that were started."""
return list(set([ return list(
svc set(
for svc_list in self.start[container_name] [
for svc in svc_list])) svc
for svc_list in self.start[container_name]
for svc in svc_list
]
)
)
def stopped_services(self, container_name: str) -> List: def stopped_services(self, container_name: str) -> List:
"""Distinct unordered list of services that were started.""" """Distinct unordered list of services that were started."""
return list(set([ return list(
svc set(
for svc_list in self.stop[container_name] [
for svc in svc_list])) svc
for svc_list in self.stop[container_name]
for svc in svc_list
]
)
)
def add_push(self, container_name: str, call: typing.Dict) -> None: def add_push(self, container_name: str, call: typing.Dict) -> None:
"""Log a push call.""" """Log a push call."""
@@ -188,18 +205,17 @@ class ContainerCalls:
def updated_files(self, container_name: str) -> typing.List: def updated_files(self, container_name: str) -> typing.List:
"""Return a list of files that have been updated in a container.""" """Return a list of files that have been updated in a container."""
return [c['path'] for c in self.push.get(container_name, [])] return [c["path"] for c in self.push.get(container_name, [])]
def file_update_calls( def file_update_calls(
self, self, container_name: str, file_name: str
container_name: str,
file_name: str
) -> typing.List: ) -> typing.List:
"""Return the update call for File_name in container_name.""" """Return the update call for File_name in container_name."""
return [ return [
c c
for c in self.push.get(container_name, []) for c in self.push.get(container_name, [])
if c['path'] == file_name] if c["path"] == file_name
]
class CharmTestCase(unittest.TestCase): class CharmTestCase(unittest.TestCase):
@@ -207,21 +223,21 @@ class CharmTestCase(unittest.TestCase):
container_calls = ContainerCalls() container_calls = ContainerCalls()
def setUp(self, obj: 'typing.ANY', patches: 'typing.List') -> None: def setUp(self, obj: "typing.ANY", patches: "typing.List") -> None:
"""Run constructor.""" """Run constructor."""
super().setUp() super().setUp()
self.patches = patches self.patches = patches
self.obj = obj self.obj = obj
self.patch_all() self.patch_all()
def patch(self, method: 'typing.ANY') -> Mock: def patch(self, method: "typing.ANY") -> Mock:
"""Patch the named method on self.obj.""" """Patch the named method on self.obj."""
_m = patch.object(self.obj, method) _m = patch.object(self.obj, method)
mock = _m.start() mock = _m.start()
self.addCleanup(_m.stop) self.addCleanup(_m.stop)
return mock return mock
def patch_obj(self, obj: 'typing.ANY', method: 'typing.ANY') -> Mock: def patch_obj(self, obj: "typing.ANY", method: "typing.ANY") -> Mock:
"""Patch the named method on obj.""" """Patch the named method on obj."""
_m = patch.object(obj, method) _m = patch.object(obj, method)
mock = _m.start() mock = _m.start()
@@ -233,10 +249,15 @@ class CharmTestCase(unittest.TestCase):
for method in self.patches: for method in self.patches:
setattr(self, method, self.patch(method)) setattr(self, method, self.patch(method))
def check_file(self, container: str, path: str, def check_file(
contents: typing.List = None, self,
user: str = None, group: str = None, container: str,
permissions: str = None) -> None: path: str,
contents: typing.List = None,
user: str = None,
group: str = None,
permissions: str = None,
) -> None:
"""Check the attributes of a file.""" """Check the attributes of a file."""
client = self.harness.charm.unit.get_container(container)._pebble client = self.harness.charm.unit.get_container(container)._pebble
files = client.list_files(path, itself=True) files = client.list_files(path, itself=True)
@@ -248,21 +269,18 @@ class CharmTestCase(unittest.TestCase):
received_data = infile.read() received_data = infile.read()
self.assertEqual(contents, received_data) self.assertEqual(contents, received_data)
if user: if user:
self.assertEqual( self.assertEqual(test_file.user, user)
test_file.user, user)
if group: if group:
self.assertEqual( self.assertEqual(test_file.group, group)
test_file.group, group)
if permissions: if permissions:
self.assertEqual( self.assertEqual(test_file.permissions, permissions)
test_file.permissions, permissions)
def add_ingress_relation(harness: Harness, endpoint_type: str) -> str: def add_ingress_relation(harness: Harness, endpoint_type: str) -> str:
"""Add ingress relation.""" """Add ingress relation."""
app_name = 'traefik-' + endpoint_type app_name = "traefik-" + endpoint_type
unit_name = app_name + '/0' unit_name = app_name + "/0"
rel_name = 'ingress-' + endpoint_type rel_name = "ingress-" + endpoint_type
rel_id = harness.add_relation(rel_name, app_name) rel_id = harness.add_relation(rel_name, app_name)
harness.add_relation_unit(rel_id, unit_name) harness.add_relation_unit(rel_id, unit_name)
return rel_id return rel_id
@@ -272,24 +290,20 @@ def add_ingress_relation_data(
harness: Harness, rel_id: str, endpoint_type: str harness: Harness, rel_id: str, endpoint_type: str
) -> None: ) -> None:
"""Add ingress data to ingress relation.""" """Add ingress data to ingress relation."""
app_name = 'traefik-' + endpoint_type app_name = "traefik-" + endpoint_type
url = 'http://' + endpoint_type + "-url" url = "http://" + endpoint_type + "-url"
ingress_data = {"url": url} ingress_data = {"url": url}
harness.update_relation_data( harness.update_relation_data(
rel_id, rel_id, app_name, {"ingress": json.dumps(ingress_data)}
app_name, )
{"ingress": json.dumps(ingress_data)})
def add_complete_ingress_relation(harness: Harness) -> None: def add_complete_ingress_relation(harness: Harness) -> None:
"""Add complete Ingress relation.""" """Add complete Ingress relation."""
for endpoint_type in ['internal', 'public']: for endpoint_type in ["internal", "public"]:
rel_id = add_ingress_relation(harness, endpoint_type) rel_id = add_ingress_relation(harness, endpoint_type)
add_ingress_relation_data( add_ingress_relation_data(harness, rel_id, endpoint_type)
harness,
rel_id,
endpoint_type)
def add_base_amqp_relation(harness: Harness) -> str: def add_base_amqp_relation(harness: Harness) -> str:
@@ -303,9 +317,7 @@ def add_base_amqp_relation(harness: Harness) -> str:
return rel_id return rel_id
def add_amqp_relation_credentials( def add_amqp_relation_credentials(harness: Harness, rel_id: str) -> None:
harness: Harness, rel_id: str
) -> None:
"""Add amqp data to amqp relation.""" """Add amqp data to amqp relation."""
harness.update_relation_data( harness.update_relation_data(
rel_id, rel_id,
@@ -390,7 +402,7 @@ def add_cloud_credentials_relation_response(
"user-domain-id": "udomain-id", "user-domain-id": "udomain-id",
"project-domain-name": "pdomain_-ame", "project-domain-name": "pdomain_-ame",
"project-domain-id": "pdomain-id", "project-domain-id": "pdomain-id",
"region": "region12" "region": "region12",
}, },
) )
@@ -406,9 +418,7 @@ def add_base_db_relation(harness: Harness) -> str:
return rel_id return rel_id
def add_db_relation_credentials( def add_db_relation_credentials(harness: Harness, rel_id: str) -> None:
harness: Harness, rel_id: str
) -> None:
"""Add db credentials data to db relation.""" """Add db credentials data to db relation."""
harness.update_relation_data( harness.update_relation_data(
rel_id, rel_id,
@@ -433,64 +443,59 @@ def add_api_relations(harness: Harness) -> None:
def add_complete_db_relation(harness: Harness) -> None: def add_complete_db_relation(harness: Harness) -> None:
"""Add complete DB relation.""" """Add complete DB relation."""
rel_id = add_base_db_relation(harness) rel_id = add_base_db_relation(harness)
add_db_relation_credentials( add_db_relation_credentials(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_complete_identity_relation(harness: Harness) -> None: def add_complete_identity_relation(harness: Harness) -> None:
"""Add complete Identity relation.""" """Add complete Identity relation."""
rel_id = add_base_identity_service_relation(harness) rel_id = add_base_identity_service_relation(harness)
add_identity_service_relation_response( add_identity_service_relation_response(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_complete_cloud_credentials_relation(harness: Harness) -> None: def add_complete_cloud_credentials_relation(harness: Harness) -> None:
"""Add complete cloud-credentials relation.""" """Add complete cloud-credentials relation."""
rel_id = add_base_cloud_credentials_relation(harness) rel_id = add_base_cloud_credentials_relation(harness)
add_cloud_credentials_relation_response( add_cloud_credentials_relation_response(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_complete_amqp_relation(harness: Harness) -> None: def add_complete_amqp_relation(harness: Harness) -> None:
"""Add complete AMQP relation.""" """Add complete AMQP relation."""
rel_id = add_base_amqp_relation(harness) rel_id = add_base_amqp_relation(harness)
add_amqp_relation_credentials( add_amqp_relation_credentials(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_ceph_relation_credentials( def add_ceph_relation_credentials(harness: Harness, rel_id: str) -> None:
harness: Harness, rel_id: str
) -> None:
"""Add amqp data to amqp relation.""" """Add amqp data to amqp relation."""
# During tests the charm class is never destroyed and recreated as it # During tests the charm class is never destroyed and recreated as it
# would be between hook executions. This means request is never marked # would be between hook executions. This means request is never marked
# as complete as it never matches the previous request and always looks # as complete as it never matches the previous request and always looks
# like it needs resending. # like it needs resending.
harness.charm.ceph.interface.previous_requests = \ harness.charm.ceph.interface.previous_requests = (
harness.charm.ceph.interface.get_previous_requests_from_relations() harness.charm.ceph.interface.get_previous_requests_from_relations()
)
request = json.loads( request = json.loads(
harness.get_relation_data(rel_id, harness.charm.unit.name)[ harness.get_relation_data(rel_id, harness.charm.unit.name)[
'broker_req']) "broker_req"
client_unit = harness.charm.unit.name.replace('/', '-') ]
)
client_unit = harness.charm.unit.name.replace("/", "-")
harness.update_relation_data( harness.update_relation_data(
rel_id, rel_id,
"ceph-mon/0", "ceph-mon/0",
{ {
'auth': 'cephx', "auth": "cephx",
'key': 'AQBUfpVeNl7CHxAA8/f6WTcYFxW2dJ5VyvWmJg==', "key": "AQBUfpVeNl7CHxAA8/f6WTcYFxW2dJ5VyvWmJg==",
'ingress-address': '192.0.2.2', "ingress-address": "192.0.2.2",
'ceph-public-address': '192.0.2.2', "ceph-public-address": "192.0.2.2",
f'broker-rsp-{client_unit}': json.dumps({ f"broker-rsp-{client_unit}": json.dumps(
'exit-code': 0, {"exit-code": 0, "request-id": request["request-id"]}
'request-id': request['request-id']})}) ),
},
)
harness.add_relation_unit(rel_id, "ceph-mon/1") harness.add_relation_unit(rel_id, "ceph-mon/1")
@@ -507,25 +512,23 @@ def add_base_ceph_relation(harness: Harness) -> str:
def add_complete_ceph_relation(harness: Harness) -> None: def add_complete_ceph_relation(harness: Harness) -> None:
"""Add complete ceph relation.""" """Add complete ceph relation."""
rel_id = add_base_ceph_relation(harness) rel_id = add_base_ceph_relation(harness)
add_ceph_relation_credentials( add_ceph_relation_credentials(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_certificates_relation_certs( def add_certificates_relation_certs(harness: Harness, rel_id: str) -> None:
harness: Harness, rel_id: str
) -> None:
"""Add cert data to certificates relation.""" """Add cert data to certificates relation."""
client_unit = harness.charm.unit.name.replace('/', '_') client_unit = harness.charm.unit.name.replace("/", "_")
harness.update_relation_data( harness.update_relation_data(
rel_id, rel_id,
'vault/0', "vault/0",
{ {
f'{client_unit}.server.cert': TEST_SERVER_CERT, f"{client_unit}.server.cert": TEST_SERVER_CERT,
f'{client_unit}.server.key': TEST_SERVER_KEY, f"{client_unit}.server.key": TEST_SERVER_KEY,
'chain': TEST_CHAIN, "chain": TEST_CHAIN,
'ca': TEST_CA}) "ca": TEST_CA,
},
)
def add_base_certificates_relation(harness: Harness) -> str: def add_base_certificates_relation(harness: Harness) -> str:
@@ -541,17 +544,13 @@ def add_base_certificates_relation(harness: Harness) -> str:
def add_complete_certificates_relation(harness: Harness) -> None: def add_complete_certificates_relation(harness: Harness) -> None:
"""Add complete certificates relation.""" """Add complete certificates relation."""
rel_id = add_base_certificates_relation(harness) rel_id = add_base_certificates_relation(harness)
add_certificates_relation_certs( add_certificates_relation_certs(harness, rel_id)
harness,
rel_id)
return rel_id return rel_id
def add_complete_peer_relation(harness: Harness) -> None: def add_complete_peer_relation(harness: Harness) -> None:
"""Add complete peer relation.""" """Add complete peer relation."""
rel_id = harness.add_relation( rel_id = harness.add_relation("peers", harness.charm.app.name)
'peers',
harness.charm.app.name)
new_unit = f"{harness.charm.app.name}/1" new_unit = f"{harness.charm.app.name}/1"
harness.add_relation_unit(rel_id, new_unit) harness.add_relation_unit(rel_id, new_unit)
harness.update_relation_data( harness.update_relation_data(
@@ -561,13 +560,14 @@ def add_complete_peer_relation(harness: Harness) -> None:
test_relations = { test_relations = {
'database': add_complete_db_relation, "database": add_complete_db_relation,
'amqp': add_complete_amqp_relation, "amqp": add_complete_amqp_relation,
'identity-service': add_complete_identity_relation, "identity-service": add_complete_identity_relation,
'cloud-credentials': add_complete_cloud_credentials_relation, "cloud-credentials": add_complete_cloud_credentials_relation,
'peers': add_complete_peer_relation, "peers": add_complete_peer_relation,
'certificates': add_complete_certificates_relation, "certificates": add_complete_certificates_relation,
'ceph': add_complete_ceph_relation} "ceph": add_complete_ceph_relation,
}
def add_all_relations(harness: Harness) -> None: def add_all_relations(harness: Harness) -> None:
@@ -606,7 +606,6 @@ def get_harness(
"""Return a testing harness.""" """Return a testing harness."""
class _OSTestingPebbleClient(_TestingPebbleClient): class _OSTestingPebbleClient(_TestingPebbleClient):
def exec( def exec(
self, self,
command: typing.List[str], command: typing.List[str],
@@ -619,34 +618,35 @@ def get_harness(
group_id: int = None, group_id: int = None,
group: str = None, group: str = None,
stdin: typing.Union[ stdin: typing.Union[
str, bytes, typing.TextIO, typing.BinaryIO] = None, str, bytes, typing.TextIO, typing.BinaryIO
] = None,
stdout: typing.Union[typing.TextIO, typing.BinaryIO] = None, stdout: typing.Union[typing.TextIO, typing.BinaryIO] = None,
stderr: typing.Union[typing.TextIO, typing.BinaryIO] = None, stderr: typing.Union[typing.TextIO, typing.BinaryIO] = None,
encoding: str = 'utf-8', encoding: str = "utf-8",
combine_stderr: bool = False combine_stderr: bool = False,
) -> None: ) -> None:
container_calls.add_execute( container_calls.add_execute(self.container_name, command)
self.container_name,
command)
process_mock = MagicMock() process_mock = MagicMock()
process_mock.wait_output.return_value = ('', None) process_mock.wait_output.return_value = ("", None)
return process_mock return process_mock
def start_services( def start_services(
self, services: List[str], timeout: float = 30.0, self,
delay: float = 0.1,) -> None: services: List[str],
timeout: float = 30.0,
delay: float = 0.1,
) -> None:
"""Record start service events.""" """Record start service events."""
container_calls.add_start( container_calls.add_start(self.container_name, services)
self.container_name,
services)
def stop_services( def stop_services(
self, services: List[str], timeout: float = 30.0, self,
delay: float = 0.1,) -> None: services: List[str],
timeout: float = 30.0,
delay: float = 0.1,
) -> None:
"""Record stop service events.""" """Record stop service events."""
container_calls.add_stop( container_calls.add_stop(self.container_name, services)
self.container_name,
services)
class _OSTestingModelBackend(_TestingModelBackend): class _OSTestingModelBackend(_TestingModelBackend):
def get_pebble(self, socket_path: str) -> _OSTestingPebbleClient: def get_pebble(self, socket_path: str) -> _OSTestingPebbleClient:
@@ -656,7 +656,7 @@ def get_harness(
client = _OSTestingPebbleClient(self) client = _OSTestingPebbleClient(self)
# Extract container name from: # Extract container name from:
# /charm/containers/placement-api/pebble.socket # /charm/containers/placement-api/pebble.socket
client.container_name = socket_path.split('/')[3] client.container_name = socket_path.split("/")[3]
self._pebble_clients[socket_path] = client self._pebble_clients[socket_path] = client
self._pebble_clients_can_connect[client] = not SIMULATE_CAN_CONNECT self._pebble_clients_can_connect[client] = not SIMULATE_CAN_CONNECT
return client return client
@@ -692,11 +692,7 @@ def get_harness(
with open(metadata_file) as f: with open(metadata_file) as f:
charm_metadata = f.read() charm_metadata = f.read()
harness = Harness( harness = Harness(charm_class, meta=charm_metadata, config=charm_config)
charm_class,
meta=charm_metadata,
config=charm_config
)
harness._backend = _OSTestingModelBackend( harness._backend = _OSTestingModelBackend(
harness._unit_name, harness._meta, harness._get_config(charm_config) harness._unit_name, harness._meta, harness._get_config(charm_config)
) )

View File

@@ -0,0 +1,39 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
# Testing tools configuration
[tool.coverage.run]
branch = true
[tool.coverage.report]
show_missing = true
[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"
# Formatting tools configuration
[tool.black]
line-length = 79
[tool.isort]
profile = "black"
multi_line_output = 3
force_grid_wrap = true
# Linting tools configuration
[tool.flake8]
max-line-length = 79
max-doc-length = 99
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "E501", "D107", "E402"]
per-file-ignores = []
docstring-convention = "google"
# Check for properly formatted copyright header in each file
copyright-check = "True"
copyright-author = "Canonical Ltd."
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"

View File

@@ -2,12 +2,20 @@
[tox] [tox]
skipsdist = True skipsdist = True
envlist = pep8,py3 envlist = lint, py3
sitepackages = False sitepackages = False
skip_missing_interpreters = False skip_missing_interpreters = False
minversion = 3.18.0 minversion = 3.18.0
requires = virtualenv < 20.0 requires = virtualenv < 20.0
[vars]
src_path = {toxinidir}/ops_sunbeam
tst_path = {toxinidir}/unit_tests/
tst_lib_path = {toxinidir}/unit_tests/lib/
pyproject_toml = {toxinidir}/pyproject.toml
cookie_cutter_path = {toxinidir}/shared_code/sunbeam_charm/\{\{cookiecutter.service_name\}\}
all_path = {[vars]src_path} {[vars]tst_path}
[testenv] [testenv]
basepython = python3 basepython = python3
install_command = install_command =
@@ -20,6 +28,15 @@ allowlist_externals =
deps = deps =
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
[testenv:fmt]
description = Apply coding style standards to code
deps =
black
isort
commands =
isort {[vars]all_path} --skip-glob {[vars]tst_lib_path} --skip {toxinidir}/.tox
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]tst_lib_path}
[testenv:fetch] [testenv:fetch]
basepython = python3 basepython = python3
deps = deps =
@@ -50,11 +67,29 @@ basepython = python3.10
deps = {[testenv:py3]deps} deps = {[testenv:py3]deps}
[testenv:pep8] [testenv:pep8]
basepython = python3 description = Alias for lint
deps = {[testenv:lint]deps}
commands = {[testenv:lint]commands}
[testenv:lint]
description = Check code against coding style standards
deps = deps =
{[testenv]deps} black
-r{toxinidir}/requirements.txt # flake8==4.0.1 # Pin version until https://github.com/csachs/pyproject-flake8/pull/14 is merged
commands = flake8 {posargs} unit_tests ops_sunbeam --exclude unit_tests/lib flake8
flake8-docstrings
flake8-copyright
flake8-builtins
pyproject-flake8
pep8-naming
isort
codespell
commands =
codespell {[vars]all_path}
# pflake8 wrapper supports config from pyproject.toml
pflake8 --exclude {[vars]tst_lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]tst_lib_path}
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]tst_lib_path}
[testenv:cover] [testenv:cover]
basepython = python3 basepython = python3

View File

@@ -14,4 +14,5 @@
"""Unit tests for aso.""" """Unit tests for aso."""
import ops.testing import ops.testing
ops.testing.SIMULATE_CAN_CONNECT = True ops.testing.SIMULATE_CAN_CONNECT = True

View File

@@ -17,14 +17,18 @@
"""Test charms for unit tests.""" """Test charms for unit tests."""
import os import os
import tempfile
import sys import sys
import tempfile
from typing import TYPE_CHECKING from typing import (
TYPE_CHECKING,
)
if TYPE_CHECKING: if TYPE_CHECKING:
import ops.framework import ops.framework
from typing import List
from typing import (
List,
)
sys.path.append("unit_tests/lib") # noqa sys.path.append("unit_tests/lib") # noqa
sys.path.append("src") # noqa sys.path.append("src") # noqa
@@ -44,9 +48,7 @@ options:
type: string type: string
""" """
INITIAL_CHARM_CONFIG = { INITIAL_CHARM_CONFIG = {"debug": "true", "region": "RegionOne"}
'debug': 'true',
'region': 'RegionOne'}
CHARM_METADATA = """ CHARM_METADATA = """
name: my-service name: my-service
@@ -206,7 +208,10 @@ class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
wsgi_admin_script = "/bin/wsgi_admin" wsgi_admin_script = "/bin/wsgi_admin"
wsgi_public_script = "/bin/wsgi_public" wsgi_public_script = "/bin/wsgi_public"
mandatory_relations = { mandatory_relations = {
"database", "amqp", "identity-service", "ingress-public" "database",
"amqp",
"identity-service",
"ingress-public",
} }
def __init__(self, framework: "ops.framework.Framework") -> None: def __init__(self, framework: "ops.framework.Framework") -> None:
@@ -256,7 +261,7 @@ class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
@property @property
def healthcheck_http_url(self) -> str: def healthcheck_http_url(self) -> str:
"""Healthcheck HTTP URL for the service.""" """Healthcheck HTTP URL for the service."""
return f'http://localhost:{self.default_public_ingress_port}/v3' return f"http://localhost:{self.default_public_ingress_port}/v3"
class MultiSvcPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): class MultiSvcPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
@@ -300,5 +305,6 @@ class TestMultiSvcCharm(MyAPICharm):
self.container_configs, self.container_configs,
self.template_dir, self.template_dir,
self.openstack_release, self.openstack_release,
self.configure_charm self.configure_charm,
)] )
]

View File

@@ -14,18 +14,27 @@
"""Test compound_status.""" """Test compound_status."""
import mock
import sys import sys
import mock
sys.path.append("lib") # noqa sys.path.append("lib") # noqa
sys.path.append("src") # noqa sys.path.append("src") # noqa
from ops.model import ActiveStatus, BlockedStatus, UnknownStatus, WaitingStatus from ops.model import (
ActiveStatus,
BlockedStatus,
UnknownStatus,
WaitingStatus,
)
import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.compound_status as compound_status import ops_sunbeam.compound_status as compound_status
import ops_sunbeam.test_utils as test_utils import ops_sunbeam.test_utils as test_utils
from . import test_charms
from . import (
test_charms,
)
class TestCompoundStatus(test_utils.CharmTestCase): class TestCompoundStatus(test_utils.CharmTestCase):

View File

@@ -14,24 +14,27 @@
"""Test aso.""" """Test aso."""
import mock
import sys import sys
sys.path.append('lib') # noqa import mock
sys.path.append('src') # noqa
sys.path.append("lib") # noqa
sys.path.append("src") # noqa
import ops.model import ops.model
import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.test_utils as test_utils import ops_sunbeam.test_utils as test_utils
from . import test_charms
from . import (
test_charms,
)
class TestOSBaseOperatorCharm(test_utils.CharmTestCase): class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
"""Test for the OSBaseOperatorCharm class.""" """Test for the OSBaseOperatorCharm class."""
PATCHES = [ PATCHES = []
]
def setUp(self) -> None: def setUp(self) -> None:
"""Charm test class setup.""" """Charm test class setup."""
@@ -42,37 +45,33 @@ class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
test_charms.CHARM_METADATA, test_charms.CHARM_METADATA,
self.container_calls, self.container_calls,
charm_config=test_charms.CHARM_CONFIG, charm_config=test_charms.CHARM_CONFIG,
initial_charm_config=test_charms.INITIAL_CHARM_CONFIG) initial_charm_config=test_charms.INITIAL_CHARM_CONFIG,
)
self.harness.begin() self.harness.begin()
self.addCleanup(self.harness.cleanup) self.addCleanup(self.harness.cleanup)
def set_pebble_ready(self) -> None: def set_pebble_ready(self) -> None:
"""Set pebble ready event.""" """Set pebble ready event."""
self.harness.container_pebble_ready('my-service') self.harness.container_pebble_ready("my-service")
def test_pebble_ready_handler(self) -> None: def test_pebble_ready_handler(self) -> None:
"""Test is raised and observed.""" """Test is raised and observed."""
self.assertEqual(self.harness.charm.seen_events, []) self.assertEqual(self.harness.charm.seen_events, [])
self.set_pebble_ready() self.set_pebble_ready()
self.assertEqual(self.harness.charm.seen_events, ['PebbleReadyEvent']) self.assertEqual(self.harness.charm.seen_events, ["PebbleReadyEvent"])
def test_write_config(self) -> None: def test_write_config(self) -> None:
"""Test writing config when charm is ready.""" """Test writing config when charm is ready."""
self.set_pebble_ready() self.set_pebble_ready()
self.assertEqual( self.assertEqual(self.container_calls.push["my-service"], [])
self.container_calls.push['my-service'],
[])
def test_container_names(self) -> None: def test_container_names(self) -> None:
"""Test container name list is correct.""" """Test container name list is correct."""
self.assertEqual( self.assertEqual(self.harness.charm.container_names, ["my-service"])
self.harness.charm.container_names,
['my-service'])
def test_relation_handlers_ready(self) -> None: def test_relation_handlers_ready(self) -> None:
"""Test relation handlers are ready.""" """Test relation handlers are ready."""
self.assertTrue( self.assertTrue(self.harness.charm.relation_handlers_ready())
self.harness.charm.relation_handlers_ready())
class _TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): class _TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
@@ -90,14 +89,16 @@ class _TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
test_charms.API_CHARM_METADATA, test_charms.API_CHARM_METADATA,
self.container_calls, self.container_calls,
charm_config=test_charms.CHARM_CONFIG, charm_config=test_charms.CHARM_CONFIG,
initial_charm_config=test_charms.INITIAL_CHARM_CONFIG) initial_charm_config=test_charms.INITIAL_CHARM_CONFIG,
)
# clean up events that were dynamically defined, # clean up events that were dynamically defined,
# otherwise we get issues because they'll be redefined, # otherwise we get issues because they'll be redefined,
# which is not allowed. # which is not allowed.
from charms.data_platform_libs.v0.database_requires import ( from charms.data_platform_libs.v0.database_requires import (
DatabaseEvents DatabaseEvents,
) )
for attr in ( for attr in (
"database_database_created", "database_database_created",
"database_endpoints_changed", "database_endpoints_changed",
@@ -113,15 +114,16 @@ class _TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
def set_pebble_ready(self) -> None: def set_pebble_ready(self) -> None:
"""Set pebble ready event.""" """Set pebble ready event."""
self.harness.container_pebble_ready('my-service') self.harness.container_pebble_ready("my-service")
class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm): class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm):
"""Test Charm with services.""" """Test Charm with services."""
@mock.patch( @mock.patch(
'charms.observability_libs.v0.kubernetes_service_patch.' "charms.observability_libs.v0.kubernetes_service_patch."
'KubernetesServicePatch') "KubernetesServicePatch"
)
def setUp(self, mock_svc_patch: mock.patch) -> None: def setUp(self, mock_svc_patch: mock.patch) -> None:
"""Run test class setup.""" """Run test class setup."""
super().setUp(test_charms.MyAPICharm) super().setUp(test_charms.MyAPICharm)
@@ -132,32 +134,33 @@ class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm):
self.harness.set_leader() self.harness.set_leader()
test_utils.add_complete_peer_relation(self.harness) test_utils.add_complete_peer_relation(self.harness)
self.set_pebble_ready() self.set_pebble_ready()
self.harness.charm.leader_set({'foo': 'bar'}) self.harness.charm.leader_set({"foo": "bar"})
test_utils.add_api_relations(self.harness) test_utils.add_api_relations(self.harness)
test_utils.add_complete_cloud_credentials_relation(self.harness) test_utils.add_complete_cloud_credentials_relation(self.harness)
expect_entries = [ expect_entries = [
'/bin/wsgi_admin', "/bin/wsgi_admin",
'hardpassword', "hardpassword",
'True', "True",
'rabbit://my-service:rabbit.pass@10.0.0.13:5672/openstack', "rabbit://my-service:rabbit.pass@10.0.0.13:5672/openstack",
'rabbithost1.local', "rabbithost1.local",
'svcpass1', "svcpass1",
'bar'] "bar",
expect_string = '\n' + '\n'.join(expect_entries) ]
self.harness.set_can_connect('my-service', True) expect_string = "\n" + "\n".join(expect_entries)
self.harness.set_can_connect("my-service", True)
self.check_file( self.check_file(
'my-service', "my-service",
'/etc/my-service/my-service.conf', "/etc/my-service/my-service.conf",
contents=expect_string, contents=expect_string,
user='my-service', user="my-service",
group='my-service', group="my-service",
) )
self.check_file( self.check_file(
'my-service', "my-service",
'/etc/apache2/sites-available/wsgi-my-service.conf', "/etc/apache2/sites-available/wsgi-my-service.conf",
contents=expect_string, contents=expect_string,
user='root', user="root",
group='root', group="root",
) )
def test_assess_status(self) -> None: def test_assess_status(self) -> None:
@@ -165,20 +168,20 @@ class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm):
test_utils.add_complete_ingress_relation(self.harness) test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader() self.harness.set_leader()
test_utils.add_complete_peer_relation(self.harness) test_utils.add_complete_peer_relation(self.harness)
self.harness.charm.leader_set({'foo': 'bar'}) self.harness.charm.leader_set({"foo": "bar"})
test_utils.add_api_relations(self.harness) test_utils.add_api_relations(self.harness)
test_utils.add_complete_cloud_credentials_relation(self.harness) test_utils.add_complete_cloud_credentials_relation(self.harness)
self.harness.set_can_connect('my-service', True) self.harness.set_can_connect("my-service", True)
self.assertNotEqual( self.assertNotEqual(
self.harness.charm.status.status, self.harness.charm.status.status, ops.model.ActiveStatus()
ops.model.ActiveStatus()) )
self.set_pebble_ready() self.set_pebble_ready()
for ph in self.harness.charm.pebble_handlers: for ph in self.harness.charm.pebble_handlers:
self.assertTrue(ph.service_ready) self.assertTrue(ph.service_ready)
self.assertEqual( self.assertEqual(
self.harness.charm.status.status, self.harness.charm.status.status, ops.model.ActiveStatus()
ops.model.ActiveStatus()) )
def test_start_services(self) -> None: def test_start_services(self) -> None:
"""Test service is started.""" """Test service is started."""
@@ -186,81 +189,62 @@ class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm):
self.harness.set_leader() self.harness.set_leader()
test_utils.add_complete_peer_relation(self.harness) test_utils.add_complete_peer_relation(self.harness)
self.set_pebble_ready() self.set_pebble_ready()
self.harness.charm.leader_set({'foo': 'bar'}) self.harness.charm.leader_set({"foo": "bar"})
test_utils.add_api_relations(self.harness) test_utils.add_api_relations(self.harness)
test_utils.add_complete_cloud_credentials_relation(self.harness) test_utils.add_complete_cloud_credentials_relation(self.harness)
self.harness.set_can_connect('my-service', True) self.harness.set_can_connect("my-service", True)
self.assertEqual( self.assertEqual(
self.container_calls.started_services('my-service'), self.container_calls.started_services("my-service"),
['wsgi-my-service']) ["wsgi-my-service"],
)
def test__on_database_changed(self) -> None: def test__on_database_changed(self) -> None:
"""Test database is requested.""" """Test database is requested."""
rel_id = self.harness.add_relation('peers', 'my-service') rel_id = self.harness.add_relation("peers", "my-service")
self.harness.add_relation_unit( self.harness.add_relation_unit(rel_id, "my-service/1")
rel_id,
'my-service/1')
self.harness.set_leader() self.harness.set_leader()
self.set_pebble_ready() self.set_pebble_ready()
db_rel_id = test_utils.add_base_db_relation(self.harness) db_rel_id = test_utils.add_base_db_relation(self.harness)
test_utils.add_db_relation_credentials(self.harness, db_rel_id) test_utils.add_db_relation_credentials(self.harness, db_rel_id)
rel_data = self.harness.get_relation_data( rel_data = self.harness.get_relation_data(db_rel_id, "my-service")
db_rel_id, requested_db = rel_data["database"]
'my-service') self.assertEqual(requested_db, "my_service")
requested_db = rel_data['database']
self.assertEqual(requested_db, 'my_service')
def test_contexts(self) -> None: def test_contexts(self) -> None:
"""Test contexts are correctly populated.""" """Test contexts are correctly populated."""
rel_id = self.harness.add_relation('peers', 'my-service') rel_id = self.harness.add_relation("peers", "my-service")
self.harness.add_relation_unit( self.harness.add_relation_unit(rel_id, "my-service/1")
rel_id,
'my-service/1')
self.harness.set_leader() self.harness.set_leader()
self.set_pebble_ready() self.set_pebble_ready()
db_rel_id = test_utils.add_base_db_relation(self.harness) db_rel_id = test_utils.add_base_db_relation(self.harness)
test_utils.add_db_relation_credentials(self.harness, db_rel_id) test_utils.add_db_relation_credentials(self.harness, db_rel_id)
contexts = self.harness.charm.contexts() contexts = self.harness.charm.contexts()
self.assertEqual( self.assertEqual(
contexts.wsgi_config.wsgi_admin_script, contexts.wsgi_config.wsgi_admin_script, "/bin/wsgi_admin"
'/bin/wsgi_admin') )
self.assertEqual( self.assertEqual(contexts.database.database_password, "hardpassword")
contexts.database.database_password, self.assertEqual(contexts.options.debug, True)
'hardpassword')
self.assertEqual(
contexts.options.debug,
True)
def test_peer_leader_db(self) -> None: def test_peer_leader_db(self) -> None:
"""Test interacting with peer app db.""" """Test interacting with peer app db."""
rel_id = self.harness.add_relation('peers', 'my-service') rel_id = self.harness.add_relation("peers", "my-service")
self.harness.add_relation_unit( self.harness.add_relation_unit(rel_id, "my-service/1")
rel_id,
'my-service/1')
self.harness.set_leader() self.harness.set_leader()
self.harness.charm.leader_set({'ready': 'true'}) self.harness.charm.leader_set({"ready": "true"})
self.harness.charm.leader_set({'foo': 'bar'}) self.harness.charm.leader_set({"foo": "bar"})
self.harness.charm.leader_set(ginger='biscuit') self.harness.charm.leader_set(ginger="biscuit")
rel_data = self.harness.get_relation_data(rel_id, 'my-service') rel_data = self.harness.get_relation_data(rel_id, "my-service")
self.assertEqual( self.assertEqual(
rel_data, rel_data, {"ready": "true", "foo": "bar", "ginger": "biscuit"}
{'ready': 'true', 'foo': 'bar', 'ginger': 'biscuit'}) )
self.assertEqual( self.assertEqual(self.harness.charm.leader_get("ready"), "true")
self.harness.charm.leader_get('ready'), self.assertEqual(self.harness.charm.leader_get("foo"), "bar")
'true') self.assertEqual(self.harness.charm.leader_get("ginger"), "biscuit")
self.assertEqual(
self.harness.charm.leader_get('foo'),
'bar')
self.assertEqual(
self.harness.charm.leader_get('ginger'),
'biscuit')
def test_peer_leader_ready(self) -> None: def test_peer_leader_ready(self) -> None:
"""Test peer leader ready methods.""" """Test peer leader ready methods."""
rel_id = self.harness.add_relation('peers', 'my-service') rel_id = self.harness.add_relation("peers", "my-service")
self.harness.add_relation_unit( self.harness.add_relation_unit(rel_id, "my-service/1")
rel_id,
'my-service/1')
self.harness.set_leader() self.harness.set_leader()
self.assertFalse(self.harness.charm.is_leader_ready()) self.assertFalse(self.harness.charm.is_leader_ready())
self.harness.charm.set_leader_ready() self.harness.charm.set_leader_ready()
@@ -271,87 +255,86 @@ class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm):
# Add ingress relation # Add ingress relation
test_utils.add_complete_ingress_relation(self.harness) test_utils.add_complete_ingress_relation(self.harness)
self.assertEqual( self.assertEqual(
self.harness.charm.internal_url, self.harness.charm.internal_url, "http://internal-url"
'http://internal-url') )
self.assertEqual( self.assertEqual(self.harness.charm.public_url, "http://public-url")
self.harness.charm.public_url,
'http://public-url')
@mock.patch('ops_sunbeam.charm.Client') @mock.patch("ops_sunbeam.charm.Client")
def test_endpoint_urls_no_ingress(self, mock_client: mock.patch) -> None: def test_endpoint_urls_no_ingress(self, mock_client: mock.patch) -> None:
"""Test public_url and internal_url with no ingress defined.""" """Test public_url and internal_url with no ingress defined."""
class mock_service:
class MockService:
"""Mock lightkube client service object.""" """Mock lightkube client service object."""
def __init__(self) -> None: def __init__(self) -> None:
self.status = None self.status = None
mock_client.return_value = mock.MagicMock() mock_client.return_value = mock.MagicMock()
mock_client.return_value.get.return_value = mock_service() mock_client.return_value.get.return_value = MockService()
self.assertEqual( self.assertEqual(
self.harness.charm.internal_url, self.harness.charm.internal_url, "http://10.0.0.10:789"
'http://10.0.0.10:789') )
self.assertEqual( self.assertEqual(self.harness.charm.public_url, "http://10.0.0.10:789")
self.harness.charm.public_url,
'http://10.0.0.10:789')
def test_relation_handlers_ready(self) -> None: def test_relation_handlers_ready(self) -> None:
"""Test relation handlers are ready.""" """Test relation handlers are ready."""
# Add all mandatory relations and test relation_handlers_ready # Add all mandatory relations and test relation_handlers_ready
db_rel_id = test_utils.add_base_db_relation(self.harness) db_rel_id = test_utils.add_base_db_relation(self.harness)
test_utils.add_db_relation_credentials(self.harness, db_rel_id) test_utils.add_db_relation_credentials(self.harness, db_rel_id)
self.assertFalse( self.assertFalse(self.harness.charm.relation_handlers_ready())
self.harness.charm.relation_handlers_ready())
amqp_rel_id = test_utils.add_base_amqp_relation(self.harness) amqp_rel_id = test_utils.add_base_amqp_relation(self.harness)
test_utils.add_amqp_relation_credentials(self.harness, amqp_rel_id) test_utils.add_amqp_relation_credentials(self.harness, amqp_rel_id)
self.assertFalse( self.assertFalse(self.harness.charm.relation_handlers_ready())
self.harness.charm.relation_handlers_ready())
identity_rel_id = test_utils.add_base_identity_service_relation( identity_rel_id = test_utils.add_base_identity_service_relation(
self.harness) self.harness
)
test_utils.add_identity_service_relation_response( test_utils.add_identity_service_relation_response(
self.harness, identity_rel_id) self.harness, identity_rel_id
self.assertFalse( )
self.harness.charm.relation_handlers_ready()) self.assertFalse(self.harness.charm.relation_handlers_ready())
ingress_rel_id = test_utils.add_ingress_relation( ingress_rel_id = test_utils.add_ingress_relation(
self.harness, 'public') self.harness, "public"
)
test_utils.add_ingress_relation_data( test_utils.add_ingress_relation_data(
self.harness, ingress_rel_id, 'public') self.harness, ingress_rel_id, "public"
self.assertTrue( )
self.harness.charm.relation_handlers_ready()) self.assertTrue(self.harness.charm.relation_handlers_ready())
# Add an optional relation and test if relation_handlers_ready # Add an optional relation and test if relation_handlers_ready
# returns True # returns True
optional_rel_id = test_utils.add_ingress_relation( optional_rel_id = test_utils.add_ingress_relation(
self.harness, 'internal') self.harness, "internal"
)
test_utils.add_ingress_relation_data( test_utils.add_ingress_relation_data(
self.harness, optional_rel_id, 'internal') self.harness, optional_rel_id, "internal"
self.assertTrue( )
self.harness.charm.relation_handlers_ready()) self.assertTrue(self.harness.charm.relation_handlers_ready())
# Remove a mandatory relation and test if relation_handlers_ready # Remove a mandatory relation and test if relation_handlers_ready
# returns False # returns False
self.harness.remove_relation(ingress_rel_id) self.harness.remove_relation(ingress_rel_id)
self.assertFalse( self.assertFalse(self.harness.charm.relation_handlers_ready())
self.harness.charm.relation_handlers_ready())
# Add the mandatory relation back and retest relation_handlers_ready # Add the mandatory relation back and retest relation_handlers_ready
ingress_rel_id = test_utils.add_ingress_relation( ingress_rel_id = test_utils.add_ingress_relation(
self.harness, 'public') self.harness, "public"
)
test_utils.add_ingress_relation_data( test_utils.add_ingress_relation_data(
self.harness, ingress_rel_id, 'public') self.harness, ingress_rel_id, "public"
self.assertTrue( )
self.harness.charm.relation_handlers_ready()) self.assertTrue(self.harness.charm.relation_handlers_ready())
class TestOSBaseOperatorMultiSVCAPICharm(_TestOSBaseOperatorAPICharm): class TestOSBaseOperatorMultiSVCAPICharm(_TestOSBaseOperatorAPICharm):
"""Test Charm with multiple services.""" """Test Charm with multiple services."""
@mock.patch( @mock.patch(
'charms.observability_libs.v0.kubernetes_service_patch.' "charms.observability_libs.v0.kubernetes_service_patch."
'KubernetesServicePatch') "KubernetesServicePatch"
)
def setUp(self, mock_svc_patch: mock.patch) -> None: def setUp(self, mock_svc_patch: mock.patch) -> None:
"""Charm test class setip.""" """Charm test class setip."""
super().setUp(test_charms.TestMultiSvcCharm) super().setUp(test_charms.TestMultiSvcCharm)
@@ -362,10 +345,11 @@ class TestOSBaseOperatorMultiSVCAPICharm(_TestOSBaseOperatorAPICharm):
self.harness.set_leader() self.harness.set_leader()
test_utils.add_complete_peer_relation(self.harness) test_utils.add_complete_peer_relation(self.harness)
self.set_pebble_ready() self.set_pebble_ready()
self.harness.charm.leader_set({'foo': 'bar'}) self.harness.charm.leader_set({"foo": "bar"})
test_utils.add_api_relations(self.harness) test_utils.add_api_relations(self.harness)
test_utils.add_complete_cloud_credentials_relation(self.harness) test_utils.add_complete_cloud_credentials_relation(self.harness)
self.harness.set_can_connect('my-service', True) self.harness.set_can_connect("my-service", True)
self.assertEqual( self.assertEqual(
sorted(self.container_calls.started_services('my-service')), sorted(self.container_calls.started_services("my-service")),
sorted(['apache forwarder', 'my-service'])) sorted(["apache forwarder", "my-service"]),
)

View File

@@ -14,25 +14,27 @@
"""Test ops_sunbeam.templating.""" """Test ops_sunbeam.templating."""
import mock
import sys import sys
from io import (
BytesIO,
TextIOWrapper,
)
import jinja2 import jinja2
from io import TextIOWrapper, BytesIO import mock
sys.path.append('lib') # noqa sys.path.append("lib") # noqa
sys.path.append('src') # noqa sys.path.append("src") # noqa
import ops_sunbeam.test_utils as test_utils
import ops_sunbeam.templating as sunbeam_templating
import ops_sunbeam.core as sunbeam_core import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.templating as sunbeam_templating
import ops_sunbeam.test_utils as test_utils
class TestTemplating(test_utils.CharmTestCase): class TestTemplating(test_utils.CharmTestCase):
"""Tests for ops_sunbeam.templating..""" """Tests for ops_sunbeam.templating.."""
PATCHES = [ PATCHES = ["get_loader"]
'get_loader'
]
def setUp(self) -> None: def setUp(self) -> None:
"""Charm test class setup.""" """Charm test class setup."""
@@ -42,41 +44,35 @@ class TestTemplating(test_utils.CharmTestCase):
"""Check rendering templates.""" """Check rendering templates."""
container_mock = mock.MagicMock() container_mock = mock.MagicMock()
config = sunbeam_core.ContainerConfigFile( config = sunbeam_core.ContainerConfigFile(
"/tmp/testfile.txt", "/tmp/testfile.txt", "myuser", "mygrp"
"myuser", )
"mygrp" self.get_loader.return_value = jinja2.DictLoader(
{"testfile.txt": "debug = {{ debug }}"}
) )
self.get_loader.return_value = jinja2.DictLoader({
'testfile.txt': 'debug = {{ debug }}'})
sunbeam_templating.sidecar_config_render( sunbeam_templating.sidecar_config_render(
container_mock, container_mock, config, "/tmp/templates", "essex", {"debug": True}
config, )
"/tmp/templates",
"essex",
{'debug': True})
container_mock.push.assert_called_once_with( container_mock.push.assert_called_once_with(
'/tmp/testfile.txt', "/tmp/testfile.txt",
'debug = True', "debug = True",
user='myuser', user="myuser",
group='mygrp', group="mygrp",
permissions=None) permissions=None,
)
def test_render_no_change(self) -> None: def test_render_no_change(self) -> None:
"""Check rendering template with no content change.""" """Check rendering template with no content change."""
container_mock = mock.MagicMock() container_mock = mock.MagicMock()
container_mock.pull.return_value = TextIOWrapper( container_mock.pull.return_value = TextIOWrapper(
BytesIO(b'debug = True')) BytesIO(b"debug = True")
config = sunbeam_core.ContainerConfigFile( )
"/tmp/testfile.txt", config = sunbeam_core.ContainerConfigFile(
"myuser", "/tmp/testfile.txt", "myuser", "mygrp"
"mygrp" )
self.get_loader.return_value = jinja2.DictLoader(
{"testfile.txt": "debug = {{ debug }}"}
) )
self.get_loader.return_value = jinja2.DictLoader({
'testfile.txt': 'debug = {{ debug }}'})
sunbeam_templating.sidecar_config_render( sunbeam_templating.sidecar_config_render(
container_mock, container_mock, config, "/tmp/templates", "essex", {"debug": True}
config, )
"/tmp/templates",
"essex",
{'debug': True})
self.assertFalse(container_mock.push.called) self.assertFalse(container_mock.push.called)