Add docstring and type hint linting
This commit is contained in:
parent
9c76f7f9f2
commit
be3b333bb0
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2021 Canonical Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Library for shared code for ops charms."""
|
@ -51,59 +51,66 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
|
|
||||||
_state = ops.framework.StoredState()
|
_state = ops.framework.StoredState()
|
||||||
|
|
||||||
def __init__(self, framework):
|
def __init__(self, framework: ops.framework.Framework) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
self._state.set_default(bootstrapped=False)
|
self._state.set_default(bootstrapped=False)
|
||||||
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.framework.observe(self.on.config_changed, self._on_config_changed)
|
||||||
self._on_config_changed)
|
|
||||||
|
|
||||||
def can_add_handler(self, relation_name, handlers):
|
def can_add_handler(
|
||||||
|
self,
|
||||||
|
relation_name: str,
|
||||||
|
handlers: List[sunbeam_rhandlers.RelationHandler],
|
||||||
|
) -> bool:
|
||||||
|
"""Whether a handler for the given relation can be added."""
|
||||||
if relation_name not in self.meta.relations.keys():
|
if relation_name not in self.meta.relations.keys():
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Cannot add handler for relation {relation_name}, relation "
|
f"Cannot add handler for relation {relation_name}, relation "
|
||||||
"not present in charm metadata")
|
"not present in charm metadata"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
if relation_name in [h.relation_name for h in handlers]:
|
if relation_name in [h.relation_name for h in handlers]:
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Cannot add handler for relation {relation_name}, handler "
|
f"Cannot add handler for relation {relation_name}, handler "
|
||||||
"already present")
|
"already present"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_relation_handlers(self, handlers=None) -> List[
|
def get_relation_handlers(
|
||||||
sunbeam_rhandlers.RelationHandler]:
|
self, handlers: List[sunbeam_rhandlers.RelationHandler] = None
|
||||||
|
) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||||
"""Relation handlers for the service."""
|
"""Relation handlers for the service."""
|
||||||
handlers = handlers or []
|
handlers = handlers or []
|
||||||
if self.can_add_handler('amqp', handlers):
|
if self.can_add_handler("amqp", handlers):
|
||||||
self.amqp = sunbeam_rhandlers.AMQPHandler(
|
self.amqp = sunbeam_rhandlers.AMQPHandler(
|
||||||
self,
|
self,
|
||||||
'amqp',
|
"amqp",
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
self.config.get('rabbit-user') or self.service_name,
|
self.config.get("rabbit-user") or self.service_name,
|
||||||
self.config.get('rabbit-vhost') or 'openstack')
|
self.config.get("rabbit-vhost") or "openstack",
|
||||||
|
)
|
||||||
handlers.append(self.amqp)
|
handlers.append(self.amqp)
|
||||||
if self.can_add_handler('shared-db', handlers):
|
if self.can_add_handler("shared-db", handlers):
|
||||||
self.db = sunbeam_rhandlers.DBHandler(
|
self.db = sunbeam_rhandlers.DBHandler(
|
||||||
self,
|
self, "shared-db", self.configure_charm, self.databases
|
||||||
'shared-db',
|
)
|
||||||
self.configure_charm,
|
|
||||||
self.databases)
|
|
||||||
handlers.append(self.db)
|
handlers.append(self.db)
|
||||||
if self.can_add_handler('ingress', handlers):
|
if self.can_add_handler("ingress", handlers):
|
||||||
self.ingress = sunbeam_rhandlers.IngressHandler(
|
self.ingress = sunbeam_rhandlers.IngressHandler(
|
||||||
self,
|
self,
|
||||||
'ingress',
|
"ingress",
|
||||||
self.service_name,
|
self.service_name,
|
||||||
self.default_public_ingress_port,
|
self.default_public_ingress_port,
|
||||||
self.configure_charm)
|
self.configure_charm,
|
||||||
|
)
|
||||||
handlers.append(self.ingress)
|
handlers.append(self.ingress)
|
||||||
if self.can_add_handler('peers', handlers):
|
if self.can_add_handler("peers", handlers):
|
||||||
self.peers = sunbeam_rhandlers.BasePeerHandler(
|
self.peers = sunbeam_rhandlers.BasePeerHandler(
|
||||||
self,
|
self, "peers", self.configure_charm
|
||||||
'peers',
|
)
|
||||||
self.configure_charm)
|
|
||||||
handlers.append(self.peers)
|
handlers.append(self.peers)
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
@ -117,12 +124,15 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
self.container_configs,
|
self.container_configs,
|
||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.openstack_release,
|
self.openstack_release,
|
||||||
self.configure_charm)]
|
self.configure_charm,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def configure_charm(self, event) -> None:
|
def configure_charm(self, event: ops.framework.EventBase) -> None:
|
||||||
"""Catchall handler to cconfigure charm services."""
|
"""Catchall handler to cconfigure charm services."""
|
||||||
if self.supports_peer_relation and not (self.unit.is_leader() or
|
if self.supports_peer_relation and not (
|
||||||
self.is_leader_ready()):
|
self.unit.is_leader() or self.is_leader_ready()
|
||||||
|
):
|
||||||
logging.debug("Leader not ready")
|
logging.debug("Leader not ready")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -148,8 +158,9 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
self._state.bootstrapped = True
|
self._state.bootstrapped = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_peer_relation(self):
|
def supports_peer_relation(self) -> bool:
|
||||||
return 'peers' in self.meta.relations.keys()
|
"""Whether the charm support the peers relation."""
|
||||||
|
return "peers" in self.meta.relations.keys()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
||||||
@ -157,26 +168,26 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_contexts(self) -> List[
|
def config_contexts(
|
||||||
sunbeam_config_contexts.CharmConfigContext]:
|
self,
|
||||||
"""Configuration adapters for the operator."""
|
) -> List[sunbeam_config_contexts.CharmConfigContext]:
|
||||||
return [
|
"""Return the configuration adapters for the operator."""
|
||||||
sunbeam_config_contexts.CharmConfigContext(self, 'options')]
|
return [sunbeam_config_contexts.CharmConfigContext(self, "options")]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handler_prefix(self) -> str:
|
def _unused_handler_prefix(self) -> str:
|
||||||
"""Prefix for handlers??"""
|
"""Prefix for handlers."""
|
||||||
return self.service_name.replace('-', '_')
|
return self.service_name.replace("-", "_")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container_names(self):
|
def container_names(self) -> List[str]:
|
||||||
"""Containers that form part of this service."""
|
"""Names of Containers that form part of this service."""
|
||||||
return [self.service_name]
|
return [self.service_name]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def template_dir(self) -> str:
|
def template_dir(self) -> str:
|
||||||
"""Directory containing Jinja2 templates."""
|
"""Directory containing Jinja2 templates."""
|
||||||
return 'src/templates'
|
return "src/templates"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def databases(self) -> List[str]:
|
def databases(self) -> List[str]:
|
||||||
@ -184,9 +195,9 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
|
|
||||||
Defaults to a single database matching the app name.
|
Defaults to a single database matching the app name.
|
||||||
"""
|
"""
|
||||||
return [self.service_name.replace('-', '_')]
|
return [self.service_name.replace("-", "_")]
|
||||||
|
|
||||||
def _on_config_changed(self, event):
|
def _on_config_changed(self, event: ops.framework.EventBase) -> None:
|
||||||
self.configure_charm(None)
|
self.configure_charm(None)
|
||||||
|
|
||||||
def containers_ready(self) -> bool:
|
def containers_ready(self) -> bool:
|
||||||
@ -212,7 +223,8 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
if handler.relation_name not in self.meta.relations.keys():
|
if handler.relation_name not in self.meta.relations.keys():
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Dropping handler for relation {handler.relation_name}, "
|
f"Dropping handler for relation {handler.relation_name}, "
|
||||||
"relation not present in charm metadata")
|
"relation not present in charm metadata"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if handler.ready:
|
if handler.ready:
|
||||||
ra.add_relation_handler(handler)
|
ra.add_relation_handler(handler)
|
||||||
@ -231,74 +243,85 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
|
|||||||
"""Determine whether the service has been boostrapped."""
|
"""Determine whether the service has been boostrapped."""
|
||||||
return self._state.bootstrapped
|
return self._state.bootstrapped
|
||||||
|
|
||||||
def leader_set(self, settings=None, **kwargs):
|
def leader_set(self, settings: dict = None, **kwargs) -> None:
|
||||||
"""Juju set data in peer data bag"""
|
"""Juju set data in peer data bag."""
|
||||||
settings = settings or {}
|
settings = settings or {}
|
||||||
settings.update(kwargs)
|
settings.update(kwargs)
|
||||||
self.peers.set_app_data(
|
self.peers.set_app_data(settings=settings)
|
||||||
settings=settings)
|
|
||||||
|
|
||||||
def leader_get(self, key: str) -> str:
|
def leader_get(self, key: str) -> str:
|
||||||
"""Retrieve data from the peer relation."""
|
"""Retrieve data from the peer relation."""
|
||||||
return self.peers.get_app_data(key)
|
return self.peers.get_app_data(key)
|
||||||
|
|
||||||
def set_leader_ready(self):
|
def set_leader_ready(self) -> None:
|
||||||
|
"""Tell peers that the leader is ready."""
|
||||||
self.peers.set_leader_ready()
|
self.peers.set_leader_ready()
|
||||||
|
|
||||||
def is_leader_ready(self):
|
def is_leader_ready(self) -> bool:
|
||||||
|
"""Has the lead unit announced that it is ready."""
|
||||||
return self.peers.is_leader_ready()
|
return self.peers.is_leader_ready()
|
||||||
|
|
||||||
|
|
||||||
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
||||||
"""Base class for OpenStack API operators"""
|
"""Base class for OpenStack API operators."""
|
||||||
|
|
||||||
def __init__(self, framework):
|
def __init__(self, framework: ops.framework.Framework) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
self._state.set_default(db_ready=False)
|
self._state.set_default(db_ready=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service_endpoints(self):
|
def service_endpoints(self) -> List[dict]:
|
||||||
|
"""List of endpoints for this service."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_relation_handlers(self, handlers=None) -> List[
|
def get_relation_handlers(
|
||||||
sunbeam_rhandlers.RelationHandler]:
|
self, handlers: List[sunbeam_rhandlers.RelationHandler] = None
|
||||||
|
) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||||
"""Relation handlers for the service."""
|
"""Relation handlers for the service."""
|
||||||
handlers = handlers or []
|
handlers = handlers or []
|
||||||
if self.can_add_handler('identity-service', handlers):
|
if self.can_add_handler("identity-service", handlers):
|
||||||
self.id_svc = sunbeam_rhandlers.IdentityServiceRequiresHandler(
|
self.id_svc = sunbeam_rhandlers.IdentityServiceRequiresHandler(
|
||||||
self,
|
self,
|
||||||
'identity-service',
|
"identity-service",
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
self.service_endpoints,
|
self.service_endpoints,
|
||||||
self.model.config['region'])
|
self.model.config["region"],
|
||||||
|
)
|
||||||
handlers.append(self.id_svc)
|
handlers.append(self.id_svc)
|
||||||
handlers = super().get_relation_handlers(handlers)
|
handlers = super().get_relation_handlers(handlers)
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
def service_url(self, hostname):
|
def service_url(self, hostname: str) -> str:
|
||||||
return f'http://{hostname}:{self.default_public_ingress_port}'
|
"""Service url for accessing this service via the given hostname."""
|
||||||
|
return f"http://{hostname}:{self.default_public_ingress_port}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_url(self):
|
def public_url(self) -> str:
|
||||||
|
"""Url for accessing the public endpoint for this service."""
|
||||||
svc_hostname = self.model.config.get(
|
svc_hostname = self.model.config.get(
|
||||||
'os-public-hostname',
|
"os-public-hostname", self.service_name
|
||||||
self.service_name)
|
)
|
||||||
return self.service_url(svc_hostname)
|
return self.service_url(svc_hostname)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def admin_url(self):
|
def admin_url(self) -> str:
|
||||||
|
"""Url for accessing the admin endpoint for this service."""
|
||||||
hostname = self.model.get_binding(
|
hostname = self.model.get_binding(
|
||||||
'identity-service').network.ingress_address
|
"identity-service"
|
||||||
|
).network.ingress_address
|
||||||
return self.service_url(hostname)
|
return self.service_url(hostname)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def internal_url(self):
|
def internal_url(self) -> str:
|
||||||
|
"""Url for accessing the internal endpoint for this service."""
|
||||||
hostname = self.model.get_binding(
|
hostname = self.model.get_binding(
|
||||||
'identity-service').network.ingress_address
|
"identity-service"
|
||||||
|
).network.ingress_address
|
||||||
return self.service_url(hostname)
|
return self.service_url(hostname)
|
||||||
|
|
||||||
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
|
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
|
||||||
"""Pebble handlers for the service"""
|
"""Pebble handlers for the service."""
|
||||||
return [
|
return [
|
||||||
sunbeam_chandlers.WSGIPebbleHandler(
|
sunbeam_chandlers.WSGIPebbleHandler(
|
||||||
self,
|
self,
|
||||||
@ -308,18 +331,24 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
|||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.openstack_release,
|
self.openstack_release,
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
f'wsgi-{self.service_name}')]
|
f"wsgi-{self.service_name}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
||||||
"""Container configuration files for the service."""
|
"""Container configuration files for the service."""
|
||||||
_cconfigs = super().container_configs
|
_cconfigs = super().container_configs
|
||||||
_cconfigs.extend([
|
_cconfigs.extend(
|
||||||
sunbeam_core.ContainerConfigFile(
|
[
|
||||||
[self.wsgi_container_name],
|
sunbeam_core.ContainerConfigFile(
|
||||||
self.service_conf,
|
[self.wsgi_container_name],
|
||||||
self.service_user,
|
self.service_conf,
|
||||||
self.service_group)])
|
self.service_user,
|
||||||
|
self.service_group,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
return _cconfigs
|
return _cconfigs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -335,16 +364,19 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
|||||||
@property
|
@property
|
||||||
def service_conf(self) -> str:
|
def service_conf(self) -> str:
|
||||||
"""Service default configuration file."""
|
"""Service default configuration file."""
|
||||||
return f'/etc/{self.service_name}/{self.service_name}.conf'
|
return f"/etc/{self.service_name}/{self.service_name}.conf"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_contexts(self) -> List[sunbeam_config_contexts.ConfigContext]:
|
def config_contexts(self) -> List[sunbeam_config_contexts.ConfigContext]:
|
||||||
"""Generate list of configuration adapters for the charm."""
|
"""Generate list of configuration adapters for the charm."""
|
||||||
_cadapters = super().config_contexts
|
_cadapters = super().config_contexts
|
||||||
_cadapters.extend([
|
_cadapters.extend(
|
||||||
sunbeam_config_contexts.WSGIWorkerConfigContext(
|
[
|
||||||
self,
|
sunbeam_config_contexts.WSGIWorkerConfigContext(
|
||||||
'wsgi_config')])
|
self, "wsgi_config"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
return _cadapters
|
return _cadapters
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -19,40 +19,56 @@ 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
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import advanced_sunbeam_openstack.charm
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContext():
|
class ConfigContext:
|
||||||
|
"""Base class used for creating a config context."""
|
||||||
|
|
||||||
def __init__(self, charm, namespace):
|
def __init__(
|
||||||
|
self,
|
||||||
|
charm: "advanced_sunbeam_openstack.charm.OSBaseOperatorCharm",
|
||||||
|
namespace: str,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.charm = charm
|
self.charm = charm
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
for k, v in self.context().items():
|
for k, v in self.context().items():
|
||||||
k = k.replace('-', '_')
|
k = k.replace("-", "_")
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self):
|
def ready(self) -> bool:
|
||||||
|
"""Whether the context has all the data is needs."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def context(self):
|
def context(self) -> dict:
|
||||||
|
"""Context used when rendering templates."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class CharmConfigContext(ConfigContext):
|
class CharmConfigContext(ConfigContext):
|
||||||
"""A context containing all of the charms config options"""
|
"""A context containing all of the charms config options."""
|
||||||
|
|
||||||
def context(self) -> dict:
|
def context(self) -> dict:
|
||||||
|
"""Charms config options."""
|
||||||
return self.charm.config
|
return self.charm.config
|
||||||
|
|
||||||
|
|
||||||
class WSGIWorkerConfigContext(ConfigContext):
|
class WSGIWorkerConfigContext(ConfigContext):
|
||||||
|
"""Configuration context for WSGI configuration."""
|
||||||
|
|
||||||
def context(self) -> dict:
|
def context(self) -> dict:
|
||||||
"""A context containing WSGI configuration options"""
|
"""WSGI configuration options."""
|
||||||
return {
|
return {
|
||||||
'name': self.charm.service_name,
|
"name": self.charm.service_name,
|
||||||
'wsgi_admin_script': self.charm.wsgi_admin_script,
|
"wsgi_admin_script": self.charm.wsgi_admin_script,
|
||||||
'wsgi_public_script': self.charm.wsgi_public_script}
|
"wsgi_public_script": self.charm.wsgi_public_script,
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Base classes for defining Pebble handlers
|
"""Base classes for defining Pebble handlers.
|
||||||
|
|
||||||
The PebbleHandler defines the pebble layers, manages pushing
|
The PebbleHandler defines the pebble layers, manages pushing
|
||||||
configuration to the containers and managing the service running
|
configuration to the containers and managing the service running
|
||||||
@ -37,11 +37,17 @@ class PebbleHandler(ops.charm.Object):
|
|||||||
|
|
||||||
_state = ops.framework.StoredState()
|
_state = ops.framework.StoredState()
|
||||||
|
|
||||||
def __init__(self, charm: ops.charm.CharmBase,
|
def __init__(
|
||||||
container_name: str, service_name: str,
|
self,
|
||||||
container_configs: List[sunbeam_core.ContainerConfigFile],
|
charm: ops.charm.CharmBase,
|
||||||
template_dir: str, openstack_release: str,
|
container_name: str,
|
||||||
callback_f: Callable):
|
service_name: str,
|
||||||
|
container_configs: List[sunbeam_core.ContainerConfigFile],
|
||||||
|
template_dir: str,
|
||||||
|
openstack_release: str,
|
||||||
|
callback_f: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
super().__init__(charm, None)
|
super().__init__(charm, None)
|
||||||
self._state.set_default(pebble_ready=False)
|
self._state.set_default(pebble_ready=False)
|
||||||
self._state.set_default(config_pushed=False)
|
self._state.set_default(config_pushed=False)
|
||||||
@ -58,52 +64,48 @@ class PebbleHandler(ops.charm.Object):
|
|||||||
|
|
||||||
def setup_pebble_handler(self) -> None:
|
def setup_pebble_handler(self) -> None:
|
||||||
"""Configure handler for pebble ready event."""
|
"""Configure handler for pebble ready event."""
|
||||||
prefix = self.container_name.replace('-', '_')
|
prefix = self.container_name.replace("-", "_")
|
||||||
pebble_ready_event = getattr(
|
pebble_ready_event = getattr(self.charm.on, f"{prefix}_pebble_ready")
|
||||||
self.charm.on,
|
self.framework.observe(
|
||||||
f'{prefix}_pebble_ready')
|
pebble_ready_event, self._on_service_pebble_ready
|
||||||
self.framework.observe(pebble_ready_event,
|
)
|
||||||
self._on_service_pebble_ready)
|
|
||||||
|
|
||||||
def _on_service_pebble_ready(self,
|
def _on_service_pebble_ready(
|
||||||
event: ops.charm.PebbleReadyEvent) -> None:
|
self, event: ops.charm.PebbleReadyEvent
|
||||||
|
) -> None:
|
||||||
"""Handle pebble ready event."""
|
"""Handle pebble ready event."""
|
||||||
container = event.workload
|
container = event.workload
|
||||||
container.add_layer(
|
container.add_layer(self.service_name, self.get_layer(), combine=True)
|
||||||
self.service_name,
|
logger.debug(f"Plan: {container.get_plan()}")
|
||||||
self.get_layer(),
|
|
||||||
combine=True)
|
|
||||||
logger.debug(f'Plan: {container.get_plan()}')
|
|
||||||
self.ready = True
|
self.ready = True
|
||||||
self._state.pebble_ready = True
|
self._state.pebble_ready = True
|
||||||
self.charm.configure_charm(event)
|
self.charm.configure_charm(event)
|
||||||
|
|
||||||
def write_config(self, context) -> None:
|
def write_config(self, context: sunbeam_core.OPSCharmContexts) -> None:
|
||||||
"""Write configuration files into the container.
|
"""Write configuration files into the container.
|
||||||
|
|
||||||
On the pre-condition that all relation adapters are ready
|
On the pre-condition that all relation adapters are ready
|
||||||
for use, write all configuration files into the container
|
for use, write all configuration files into the container
|
||||||
so that the underlying service may be started.
|
so that the underlying service may be started.
|
||||||
"""
|
"""
|
||||||
container = self.charm.unit.get_container(
|
container = self.charm.unit.get_container(self.container_name)
|
||||||
self.container_name)
|
|
||||||
if container:
|
if container:
|
||||||
sunbeam_templating.sidecar_config_render(
|
sunbeam_templating.sidecar_config_render(
|
||||||
[container],
|
[container],
|
||||||
self.container_configs,
|
self.container_configs,
|
||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.openstack_release,
|
self.openstack_release,
|
||||||
context)
|
context,
|
||||||
|
)
|
||||||
self._state.config_pushed = True
|
self._state.config_pushed = True
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug("Container not ready")
|
||||||
'Container not ready')
|
|
||||||
|
|
||||||
def get_layer(self) -> dict:
|
def get_layer(self) -> dict:
|
||||||
"""Pebble configuration layer for the container"""
|
"""Pebble configuration layer for the container."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def init_service(self, context) -> None:
|
def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None:
|
||||||
"""Initialise service ready for use.
|
"""Initialise service ready for use.
|
||||||
|
|
||||||
Write configuration files to the container and record
|
Write configuration files to the container and record
|
||||||
@ -112,8 +114,9 @@ class PebbleHandler(ops.charm.Object):
|
|||||||
self.write_config(context)
|
self.write_config(context)
|
||||||
self._state.service_ready = True
|
self._state.service_ready = True
|
||||||
|
|
||||||
def default_container_configs(self) -> List[
|
def default_container_configs(
|
||||||
sunbeam_core.ContainerConfigFile]:
|
self,
|
||||||
|
) -> List[sunbeam_core.ContainerConfigFile]:
|
||||||
"""Generate default container configurations.
|
"""Generate default container configurations.
|
||||||
|
|
||||||
These should be used by all inheriting classes and are
|
These should be used by all inheriting classes and are
|
||||||
@ -141,23 +144,37 @@ class PebbleHandler(ops.charm.Object):
|
|||||||
class WSGIPebbleHandler(PebbleHandler):
|
class WSGIPebbleHandler(PebbleHandler):
|
||||||
"""WSGI oriented handler for a Pebble managed container."""
|
"""WSGI oriented handler for a Pebble managed container."""
|
||||||
|
|
||||||
def __init__(self, charm: ops.charm.CharmBase,
|
def __init__(
|
||||||
container_name: str, service_name: str,
|
self,
|
||||||
container_configs: List[sunbeam_core.ContainerConfigFile],
|
charm: ops.charm.CharmBase,
|
||||||
template_dir: str, openstack_release: str,
|
container_name: str,
|
||||||
callback_f: Callable,
|
service_name: str,
|
||||||
wsgi_service_name: str):
|
container_configs: List[sunbeam_core.ContainerConfigFile],
|
||||||
super().__init__(charm, container_name, service_name,
|
template_dir: str,
|
||||||
container_configs, template_dir, openstack_release,
|
openstack_release: str,
|
||||||
callback_f)
|
callback_f: Callable,
|
||||||
|
wsgi_service_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
|
super().__init__(
|
||||||
|
charm,
|
||||||
|
container_name,
|
||||||
|
service_name,
|
||||||
|
container_configs,
|
||||||
|
template_dir,
|
||||||
|
openstack_release,
|
||||||
|
callback_f,
|
||||||
|
)
|
||||||
self.wsgi_service_name = wsgi_service_name
|
self.wsgi_service_name = wsgi_service_name
|
||||||
|
|
||||||
def start_wsgi(self) -> None:
|
def start_wsgi(self) -> None:
|
||||||
"""Start WSGI service"""
|
"""Start WSGI service."""
|
||||||
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 wgi service.')
|
f"{self.container_name} container is not ready. "
|
||||||
|
"Cannot start wgi service."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
service = container.get_service(self.wsgi_service_name)
|
service = container.get_service(self.wsgi_service_name)
|
||||||
if service.is_running():
|
if service.is_running():
|
||||||
@ -166,34 +183,35 @@ class WSGIPebbleHandler(PebbleHandler):
|
|||||||
container.start(self.wsgi_service_name)
|
container.start(self.wsgi_service_name)
|
||||||
|
|
||||||
def get_layer(self) -> dict:
|
def get_layer(self) -> dict:
|
||||||
"""Apache WSGI service pebble layer
|
"""Apache WSGI service pebble layer.
|
||||||
|
|
||||||
:returns: pebble layer configuration for wsgi service
|
:returns: pebble layer configuration for wsgi service
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'summary': f'{self.service_name} layer',
|
"summary": f"{self.service_name} layer",
|
||||||
'description': 'pebble config layer for apache wsgi',
|
"description": "pebble config layer for apache wsgi",
|
||||||
'services': {
|
"services": {
|
||||||
f'{self.wsgi_service_name}': {
|
f"{self.wsgi_service_name}": {
|
||||||
'override': 'replace',
|
"override": "replace",
|
||||||
'summary': f'{self.service_name} wsgi',
|
"summary": f"{self.service_name} wsgi",
|
||||||
'command': '/usr/sbin/apache2ctl -DFOREGROUND',
|
"command": "/usr/sbin/apache2ctl -DFOREGROUND",
|
||||||
'startup': 'disabled',
|
"startup": "disabled",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def init_service(self, context) -> None:
|
def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None:
|
||||||
"""Enable and start WSGI service"""
|
"""Enable and start WSGI service."""
|
||||||
container = self.charm.unit.get_container(self.container_name)
|
container = self.charm.unit.get_container(self.container_name)
|
||||||
self.write_config(context)
|
self.write_config(context)
|
||||||
try:
|
try:
|
||||||
sunbeam_cprocess.check_output(
|
sunbeam_cprocess.check_output(
|
||||||
container,
|
container, f"a2ensite {self.wsgi_service_name} && sleep 1"
|
||||||
f'a2ensite {self.wsgi_service_name} && sleep 1')
|
)
|
||||||
except sunbeam_cprocess.ContainerProcessError:
|
except sunbeam_cprocess.ContainerProcessError:
|
||||||
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"
|
||||||
|
)
|
||||||
# ignore for now - pebble is raising an exited too quickly, but it
|
# ignore for now - pebble is raising an exited too quickly, but it
|
||||||
# appears to work properly.
|
# appears to work properly.
|
||||||
self.start_wsgi()
|
self.start_wsgi()
|
||||||
@ -201,13 +219,15 @@ class WSGIPebbleHandler(PebbleHandler):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def wsgi_conf(self) -> str:
|
def wsgi_conf(self) -> str:
|
||||||
return f'/etc/apache2/sites-available/wsgi-{self.service_name}.conf'
|
"""Location of WSGI config file."""
|
||||||
|
return f"/etc/apache2/sites-available/wsgi-{self.service_name}.conf"
|
||||||
|
|
||||||
def default_container_configs(self) -> List[
|
def default_container_configs(
|
||||||
sunbeam_core.ContainerConfigFile]:
|
self,
|
||||||
|
) -> List[sunbeam_core.ContainerConfigFile]:
|
||||||
|
"""Container configs for WSGI service."""
|
||||||
return [
|
return [
|
||||||
sunbeam_core.ContainerConfigFile(
|
sunbeam_core.ContainerConfigFile(
|
||||||
[self.container_name],
|
[self.container_name], self.wsgi_conf, "root", "root"
|
||||||
self.wsgi_conf,
|
)
|
||||||
'root',
|
]
|
||||||
'root')]
|
|
||||||
|
@ -1,38 +1,69 @@
|
|||||||
|
# Copyright 2021 Canonical Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Collection of core components."""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
from typing import Generator, List, TYPE_CHECKING, Tuple, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from advanced_sunbeam_openstack.charm import OSBaseOperatorCharm
|
||||||
|
from advanced_sunbeam_openstack.config_contexts import ConfigContext
|
||||||
|
from advanced_sunbeam_openstack.relation_handlers import RelationHandler
|
||||||
|
|
||||||
ContainerConfigFile = collections.namedtuple(
|
ContainerConfigFile = collections.namedtuple(
|
||||||
'ContainerConfigFile',
|
"ContainerConfigFile", ["container_names", "path", "user", "group"]
|
||||||
['container_names', 'path', 'user', 'group'])
|
)
|
||||||
|
|
||||||
|
|
||||||
class OPSCharmContexts():
|
class OPSCharmContexts:
|
||||||
|
"""Set of config contexts and contexts from relation handlers."""
|
||||||
|
|
||||||
def __init__(self, charm):
|
def __init__(self, charm: "OSBaseOperatorCharm") -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.charm = charm
|
self.charm = charm
|
||||||
self.namespaces = []
|
self.namespaces = []
|
||||||
|
|
||||||
def add_relation_handler(self, handler):
|
def add_relation_handler(self, handler: "RelationHandler") -> None:
|
||||||
|
"""Add relation handler."""
|
||||||
interface, relation_name = handler.get_interface()
|
interface, relation_name = handler.get_interface()
|
||||||
_ns = relation_name.replace("-", "_")
|
_ns = relation_name.replace("-", "_")
|
||||||
self.namespaces.append(_ns)
|
self.namespaces.append(_ns)
|
||||||
ctxt = handler.context()
|
ctxt = handler.context()
|
||||||
obj_name = ''.join([w.capitalize() for w in relation_name.split('-')])
|
obj_name = "".join([w.capitalize() for w in relation_name.split("-")])
|
||||||
obj = collections.namedtuple(obj_name, ctxt.keys())(*ctxt.values())
|
obj = collections.namedtuple(obj_name, ctxt.keys())(*ctxt.values())
|
||||||
setattr(self, _ns, obj)
|
setattr(self, _ns, obj)
|
||||||
|
|
||||||
def add_config_contexts(self, config_adapters):
|
def add_config_contexts(
|
||||||
|
self, config_adapters: List["ConfigContext"]
|
||||||
|
) -> None:
|
||||||
|
"""Add multiple config contexts."""
|
||||||
for config_adapter in config_adapters:
|
for config_adapter in config_adapters:
|
||||||
self.add_config_context(
|
self.add_config_context(config_adapter, config_adapter.namespace)
|
||||||
config_adapter,
|
|
||||||
config_adapter.namespace)
|
|
||||||
|
|
||||||
def add_config_context(self, config_adapter, namespace):
|
def add_config_context(
|
||||||
|
self, config_adapter: "ConfigContext", namespace: str
|
||||||
|
) -> None:
|
||||||
|
"""Add add config adapater to context."""
|
||||||
self.namespaces.append(namespace)
|
self.namespaces.append(namespace)
|
||||||
setattr(self, namespace, config_adapter)
|
setattr(self, namespace, config_adapter)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(
|
||||||
"""
|
self,
|
||||||
Iterate over the relations presented to the charm.
|
) -> Generator[
|
||||||
"""
|
Tuple[str, Union["ConfigContext", "RelationHandler"]], None, None
|
||||||
|
]:
|
||||||
|
"""Iterate over the relations presented to the charm."""
|
||||||
for namespace in self.namespaces:
|
for namespace in self.namespaces:
|
||||||
yield namespace, getattr(self, namespace)
|
yield namespace, getattr(self, namespace)
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Module for running commands in containers."""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
import time
|
||||||
@ -44,49 +46,56 @@ class ContainerProcess(object):
|
|||||||
:param tmp_dir: the dir containing the location of process files
|
:param tmp_dir: the dir containing the location of process files
|
||||||
:type tmp_dir: str
|
:type tmp_dir: str
|
||||||
"""
|
"""
|
||||||
def __init__(self, container: model.Container, process_name: str,
|
|
||||||
tmp_dir: str):
|
def __init__(
|
||||||
|
self, container: model.Container, process_name: str, tmp_dir: str
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.container = weakref.proxy(container)
|
self.container = weakref.proxy(container)
|
||||||
self.process_name = process_name
|
self.process_name = process_name
|
||||||
self._returncode = RETURN_CODE_UNKNOWN
|
self._returncode = RETURN_CODE_UNKNOWN
|
||||||
self.tmp_dir = tmp_dir
|
self.tmp_dir = tmp_dir
|
||||||
self.stdout_file = f'{tmp_dir}/{process_name}.stdout'
|
self.stdout_file = f"{tmp_dir}/{process_name}.stdout"
|
||||||
self.stderr_file = f'{tmp_dir}/{process_name}.stderr'
|
self.stderr_file = f"{tmp_dir}/{process_name}.stderr"
|
||||||
self._env = dict()
|
self._env = dict()
|
||||||
self.env_file = f'{tmp_dir}/{process_name}.env'
|
self.env_file = f"{tmp_dir}/{process_name}.env"
|
||||||
self.rc_file = f'{tmp_dir}/{process_name}.rc'
|
self.rc_file = f"{tmp_dir}/{process_name}.rc"
|
||||||
self._cleaned = False
|
self._cleaned = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stdout(self) -> typing.Union[typing.BinaryIO, typing.TextIO]:
|
def stdout(self) -> typing.Union[typing.BinaryIO, typing.TextIO]:
|
||||||
return self.container.pull(f'{self.stdout_file}')
|
"""STDOUT of process."""
|
||||||
|
return self.container.pull(f"{self.stdout_file}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stderr(self) -> typing.Union[typing.BinaryIO, typing.TextIO]:
|
def stderr(self) -> typing.Union[typing.BinaryIO, typing.TextIO]:
|
||||||
return self.container.pull(f'{self.stderr_file}')
|
"""STDERR of process."""
|
||||||
|
return self.container.pull(f"{self.stderr_file}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def env(self) -> typing.Dict[str, str]:
|
def env(self) -> typing.Dict[str, str]:
|
||||||
|
"""Environment definition from container."""
|
||||||
if self._env:
|
if self._env:
|
||||||
return self._env
|
return self._env
|
||||||
|
|
||||||
with self.container.pull(f'{self.env_file}') as f:
|
with self.container.pull(f"{self.env_file}") as f:
|
||||||
for env_vars in f.read().split(b'\n'):
|
for env_vars in f.read().split(b"\n"):
|
||||||
key_values = env_vars.split(b'=', 1)
|
key_values = env_vars.split(b"=", 1)
|
||||||
self._env[key_values[0]] = key_values[1]
|
self._env[key_values[0]] = key_values[1]
|
||||||
|
|
||||||
return self._env
|
return self._env
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def returncode(self) -> int:
|
def returncode(self) -> int:
|
||||||
|
"""Return code from the process."""
|
||||||
if self._returncode == RETURN_CODE_UNKNOWN:
|
if self._returncode == RETURN_CODE_UNKNOWN:
|
||||||
self._returncode = self._get_returncode()
|
self._returncode = self._get_returncode()
|
||||||
return self._returncode
|
return self._returncode
|
||||||
|
|
||||||
def _get_returncode(self):
|
def _get_returncode(self) -> int:
|
||||||
"""Reads the contents of the returncode file"""
|
"""Read the contents of the returncode file."""
|
||||||
try:
|
try:
|
||||||
with self.container.pull(f'{self.rc_file}') as text:
|
with self.container.pull(f"{self.rc_file}") as text:
|
||||||
return int(text.read())
|
return int(text.read())
|
||||||
except pebble.PathError:
|
except pebble.PathError:
|
||||||
# If the rc file doesn't exist within the container, then the
|
# If the rc file doesn't exist within the container, then the
|
||||||
@ -95,9 +104,10 @@ class ContainerProcess(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def completed(self) -> bool:
|
def completed(self) -> bool:
|
||||||
|
"""Whether process has completed."""
|
||||||
return self._returncode != RETURN_CODE_UNKNOWN
|
return self._returncode != RETURN_CODE_UNKNOWN
|
||||||
|
|
||||||
def check_returncode(self):
|
def check_returncode(self) -> None:
|
||||||
"""Raise CalledProcessError if the exit code is non-zero."""
|
"""Raise CalledProcessError if the exit code is non-zero."""
|
||||||
if self.returncode:
|
if self.returncode:
|
||||||
stdout = None
|
stdout = None
|
||||||
@ -110,11 +120,12 @@ class ContainerProcess(object):
|
|||||||
stderr = self.stderr.read()
|
stderr = self.stderr.read()
|
||||||
except pebble.PathError:
|
except pebble.PathError:
|
||||||
pass
|
pass
|
||||||
raise CalledProcessError(self.returncode, self.process_name,
|
raise CalledProcessError(
|
||||||
stdout, stderr)
|
self.returncode, self.process_name, stdout, stderr
|
||||||
|
)
|
||||||
|
|
||||||
def wait(self, timeout: int = 30) -> None:
|
def wait(self, timeout: int = 30) -> None:
|
||||||
"""Waits for the process to complete.
|
"""Wait for the process to complete.
|
||||||
|
|
||||||
Waits for the process to complete. If the process has not completed
|
Waits for the process to complete. If the process has not completed
|
||||||
within the timeout specified, this method will raise a TimeoutExpired
|
within the timeout specified, this method will raise a TimeoutExpired
|
||||||
@ -150,11 +161,13 @@ class ContainerProcess(object):
|
|||||||
if self._cleaned:
|
if self._cleaned:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.container.remove_path(f'{self.tmp_dir}', recursive=True)
|
self.container.remove_path(f"{self.tmp_dir}", recursive=True)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
"""On destruction of this process, we'll attempt to clean up left over
|
"""On destruction of this process, cleanup.
|
||||||
process files.
|
|
||||||
|
On destruction of this process, attempt to clean up left over process
|
||||||
|
files.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
@ -164,11 +177,14 @@ class ContainerProcess(object):
|
|||||||
|
|
||||||
class ContainerProcessError(Exception):
|
class ContainerProcessError(Exception):
|
||||||
"""Base class for exceptions raised within this module."""
|
"""Base class for exceptions raised within this module."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CalledProcessError(ContainerProcessError):
|
class CalledProcessError(ContainerProcessError):
|
||||||
"""Raised when an error occurs running a process in a container and
|
"""Exception when an error occurs running a process in a container.
|
||||||
|
|
||||||
|
Raised when an error occurs running a process in a container and
|
||||||
the check=True has been passed to raise an error on failure.
|
the check=True has been passed to raise an error on failure.
|
||||||
|
|
||||||
:param returncode: the exit code from the program
|
:param returncode: the exit code from the program
|
||||||
@ -180,8 +196,15 @@ class CalledProcessError(ContainerProcessError):
|
|||||||
:param stderr: the output of the command on standard err
|
:param stderr: the output of the command on standard err
|
||||||
:type stderr: str
|
:type stderr: str
|
||||||
"""
|
"""
|
||||||
def __init__(self, returncode: int, cmd: typing.Union[str, list],
|
|
||||||
stdout: str = None, stderr: str = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
returncode: int,
|
||||||
|
cmd: typing.Union[str, list],
|
||||||
|
stdout: str = None,
|
||||||
|
stderr: str = None,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.returncode = returncode
|
self.returncode = returncode
|
||||||
self.cmd = cmd
|
self.cmd = cmd
|
||||||
self.stdout = stdout
|
self.stdout = stdout
|
||||||
@ -189,25 +212,32 @@ class CalledProcessError(ContainerProcessError):
|
|||||||
|
|
||||||
|
|
||||||
class TimeoutExpired(ContainerProcessError):
|
class TimeoutExpired(ContainerProcessError):
|
||||||
"""This exception is raised when the timeout expires while waiting for a
|
"""Exception raised when timeout expires waiting for container process.
|
||||||
container process.
|
|
||||||
|
|
||||||
:param cmd: the command that was run
|
:param cmd: the command that was run
|
||||||
:type cmd: list
|
:type cmd: list
|
||||||
:param timeout: the configured timeout for the command
|
:param timeout: the configured timeout for the command
|
||||||
:type timeout: int
|
:type timeout: int
|
||||||
"""
|
"""
|
||||||
def __init__(self, cmd: typing.Union[str, list], timeout: int):
|
|
||||||
|
def __init__(self, cmd: typing.Union[str, list], timeout: int) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.cmd = cmd
|
self.cmd = cmd
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
|
"""Message to accompany exception."""
|
||||||
return f"Command '{self.cmd}' timed out after {self.timeout} seconds"
|
return f"Command '{self.cmd}' timed out after {self.timeout} seconds"
|
||||||
|
|
||||||
|
|
||||||
def run(container: model.Container, args: typing.List[str],
|
def run(
|
||||||
timeout: int = 30, check: bool = False,
|
container: model.Container,
|
||||||
env: dict = None, service_name: str = None) -> ContainerProcess:
|
args: typing.List[str],
|
||||||
|
timeout: int = 30,
|
||||||
|
check: bool = False,
|
||||||
|
env: dict = None,
|
||||||
|
service_name: str = None,
|
||||||
|
) -> ContainerProcess:
|
||||||
"""Run command with arguments in the specified container.
|
"""Run command with arguments in the specified container.
|
||||||
|
|
||||||
Run a command in the specified container and returns a
|
Run a command in the specified container and returns a
|
||||||
@ -233,23 +263,27 @@ def run(container: model.Container, args: typing.List[str],
|
|||||||
:rtype: ContainerProcess
|
:rtype: ContainerProcess
|
||||||
"""
|
"""
|
||||||
if not container:
|
if not container:
|
||||||
raise ValueError('container cannot be None')
|
raise ValueError("container cannot be None")
|
||||||
if not isinstance(container, model.Container):
|
if not isinstance(container, model.Container):
|
||||||
raise ValueError('container must be of type ops.model.Container, '
|
raise ValueError(
|
||||||
f'not of type {type(container)}')
|
"container must be of type ops.model.Container, "
|
||||||
|
f"not of type {type(container)}"
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
if service_name is None:
|
if service_name is None:
|
||||||
service_name = args.split(' ')[0]
|
service_name = args.split(" ")[0]
|
||||||
service_name = service_name.split('/')[-1]
|
service_name = service_name.split("/")[-1]
|
||||||
cmdline = args
|
cmdline = args
|
||||||
elif isinstance(args, list):
|
elif isinstance(args, list):
|
||||||
if service_name is None:
|
if service_name is None:
|
||||||
service_name = args[0].split('/')[-1]
|
service_name = args[0].split("/")[-1]
|
||||||
cmdline = subprocess.list2cmdline(args)
|
cmdline = subprocess.list2cmdline(args)
|
||||||
else:
|
else:
|
||||||
raise ValueError('args are expected to be a str or a list of str.'
|
raise ValueError(
|
||||||
f' Provided {type(args)}')
|
"args are expected to be a str or a list of str."
|
||||||
|
f" Provided {type(args)}"
|
||||||
|
)
|
||||||
|
|
||||||
tmp_dir = f'/tmp/{service_name}-{str(uuid.uuid4()).split("-")[0]}'
|
tmp_dir = f'/tmp/{service_name}-{str(uuid.uuid4()).split("-")[0]}'
|
||||||
process = ContainerProcess(container, service_name, tmp_dir)
|
process = ContainerProcess(container, service_name, tmp_dir)
|
||||||
@ -265,50 +299,62 @@ def run(container: model.Container, args: typing.List[str],
|
|||||||
"""
|
"""
|
||||||
command = textwrap.dedent(command)
|
command = textwrap.dedent(command)
|
||||||
|
|
||||||
container.push(path=f'/tmp/{service_name}.sh', source=command,
|
container.push(
|
||||||
encoding='utf-8', permissions=0o755)
|
path=f"/tmp/{service_name}.sh",
|
||||||
|
source=command,
|
||||||
|
encoding="utf-8",
|
||||||
|
permissions=0o755,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f'Adding layer for {service_name} to run command '
|
logger.debug(
|
||||||
f'{cmdline}')
|
f"Adding layer for {service_name} to run command " f"{cmdline}"
|
||||||
container.add_layer('process_layer', {
|
)
|
||||||
'summary': 'container process runner',
|
container.add_layer(
|
||||||
'description': 'layer for running single-shot commands (kinda)',
|
"process_layer",
|
||||||
'services': {
|
{
|
||||||
service_name: {
|
"summary": "container process runner",
|
||||||
'override': 'replace',
|
"description": "layer for running single-shot commands (kinda)",
|
||||||
'summary': cmdline,
|
"services": {
|
||||||
'command': f'/tmp/{service_name}.sh',
|
service_name: {
|
||||||
'startup': 'disabled',
|
"override": "replace",
|
||||||
'environment': env or {},
|
"summary": cmdline,
|
||||||
}
|
"command": f"/tmp/{service_name}.sh",
|
||||||
}
|
"startup": "disabled",
|
||||||
}, combine=True)
|
"environment": env or {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
combine=True,
|
||||||
|
)
|
||||||
|
|
||||||
timeout_at = time.time() + timeout
|
timeout_at = time.time() + timeout
|
||||||
try:
|
try:
|
||||||
# Start the service which will run the command.
|
# Start the service which will run the command.
|
||||||
logger.debug(f'Starting {service_name} via pebble')
|
logger.debug(f"Starting {service_name} via pebble")
|
||||||
|
|
||||||
# TODO(wolsen) this is quite naughty, but the container object
|
# TODO(wolsen) this is quite naughty, but the container object
|
||||||
# doesn't provide us access to the pebble layer to specify
|
# doesn't provide us access to the pebble layer to specify
|
||||||
# timeouts and such. Some commands may need a longer time to
|
# timeouts and such. Some commands may need a longer time to
|
||||||
# start, and as such I'm using the private internal reference
|
# start, and as such I'm using the private internal reference
|
||||||
# in order to be able to specify the timeout itself.
|
# in order to be able to specify the timeout itself.
|
||||||
container._pebble.start_services([service_name], # noqa
|
container._pebble.start_services(
|
||||||
timeout=float(timeout))
|
[service_name], timeout=float(timeout) # noqa
|
||||||
|
)
|
||||||
except pebble.ChangeError:
|
except pebble.ChangeError:
|
||||||
# Check to see if the command has timed out and if so, raise
|
# Check to see if the command has timed out and if so, raise
|
||||||
# the TimeoutExpired.
|
# the TimeoutExpired.
|
||||||
if time.time() >= timeout_at:
|
if time.time() >= timeout_at:
|
||||||
logger.error(f'Command {cmdline} could not start out after '
|
logger.error(
|
||||||
f'{timeout} seconds in container '
|
f"Command {cmdline} could not start out after "
|
||||||
f'{container.name}')
|
f"{timeout} seconds in container "
|
||||||
|
f"{container.name}"
|
||||||
|
)
|
||||||
raise TimeoutExpired(args, timeout)
|
raise TimeoutExpired(args, timeout)
|
||||||
|
|
||||||
# Note, this could be expected.
|
# Note, this could be expected.
|
||||||
logger.exception(f'Error running {service_name}')
|
logger.exception(f"Error running {service_name}")
|
||||||
|
|
||||||
logger.debug('Waiting for process completion...')
|
logger.debug("Waiting for process completion...")
|
||||||
process.wait(timeout)
|
process.wait(timeout)
|
||||||
|
|
||||||
# It appears that pebble services are still active after the command
|
# It appears that pebble services are still active after the command
|
||||||
@ -320,16 +366,20 @@ def run(container: model.Container, args: typing.List[str],
|
|||||||
except pebble.ChangeError as e:
|
except pebble.ChangeError as e:
|
||||||
# Eat the change error that might occur. This was a best effort
|
# Eat the change error that might occur. This was a best effort
|
||||||
# attempt to ensure the process is stopped
|
# attempt to ensure the process is stopped
|
||||||
logger.exception(f'Failed to stop service {service_name}', e)
|
logger.exception(f"Failed to stop service {service_name}", e)
|
||||||
|
|
||||||
if check:
|
if check:
|
||||||
process.check_returncode()
|
process.check_returncode()
|
||||||
return process
|
return process
|
||||||
|
|
||||||
|
|
||||||
def call(container: model.Container, args: typing.Union[str, list],
|
def call(
|
||||||
env: dict = None, timeout: int = 30) -> int:
|
container: model.Container,
|
||||||
"""Runs a command in the container.
|
args: typing.Union[str, list],
|
||||||
|
env: dict = None,
|
||||||
|
timeout: int = 30,
|
||||||
|
) -> int:
|
||||||
|
"""Run a command in the container.
|
||||||
|
|
||||||
The command will run until the process completes (either normally or
|
The command will run until the process completes (either normally or
|
||||||
abnormally) or until the timeout expires.
|
abnormally) or until the timeout expires.
|
||||||
@ -349,17 +399,39 @@ def call(container: model.Container, args: typing.Union[str, list],
|
|||||||
return run(container, args, env=env, timeout=timeout).returncode
|
return run(container, args, env=env, timeout=timeout).returncode
|
||||||
|
|
||||||
|
|
||||||
def check_call(container: model.Container, args: typing.Union[str, list],
|
def check_call(
|
||||||
env: dict = None, timeout: int = 30,
|
container: model.Container,
|
||||||
service_name: str = None) -> None:
|
args: typing.Union[str, list],
|
||||||
run(container, args, env=env, check=True, timeout=timeout,
|
env: dict = None,
|
||||||
service_name=service_name)
|
timeout: int = 30,
|
||||||
|
service_name: str = None,
|
||||||
|
) -> None:
|
||||||
|
"""Run command and check it succeeds."""
|
||||||
|
run(
|
||||||
|
container,
|
||||||
|
args,
|
||||||
|
env=env,
|
||||||
|
check=True,
|
||||||
|
timeout=timeout,
|
||||||
|
service_name=service_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_output(container: model.Container, args: typing.Union[str, list],
|
def check_output(
|
||||||
env: dict = None, timeout: int = 30,
|
container: model.Container,
|
||||||
service_name: str = None) -> str:
|
args: typing.Union[str, list],
|
||||||
process = run(container, args, env=env, check=True, timeout=timeout,
|
env: dict = None,
|
||||||
service_name=service_name)
|
timeout: int = 30,
|
||||||
|
service_name: str = None,
|
||||||
|
) -> str:
|
||||||
|
"""Return output from command execution."""
|
||||||
|
process = run(
|
||||||
|
container,
|
||||||
|
args,
|
||||||
|
env=env,
|
||||||
|
check=True,
|
||||||
|
timeout=timeout,
|
||||||
|
service_name=service_name,
|
||||||
|
)
|
||||||
with process.stdout as stdout:
|
with process.stdout as stdout:
|
||||||
return stdout.read()
|
return stdout.read()
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Module to handle errors and bailing out of an event/hook."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
@ -22,20 +24,27 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GuardException(Exception):
|
class GuardException(Exception):
|
||||||
|
"""GuardException."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BlockedException(Exception):
|
class BlockedException(Exception):
|
||||||
|
"""Charm is blocked."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def guard(charm: 'CharmBase',
|
def guard(
|
||||||
section: str,
|
charm: "CharmBase",
|
||||||
handle_exception: bool = True,
|
section: str,
|
||||||
log_traceback: bool = True,
|
handle_exception: bool = True,
|
||||||
**__):
|
log_traceback: bool = True,
|
||||||
|
**__
|
||||||
|
) -> 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.
|
||||||
|
|
||||||
The nature of Juju is that all the information may not be available to run
|
The nature of Juju is that all the information may not be available to run
|
||||||
a set of actions. This context manager allows a section of code to be
|
a set of actions. This context manager allows a section of code to be
|
||||||
'guarded' so that it can be bailed at any time.
|
'guarded' so that it can be bailed at any time.
|
||||||
@ -54,21 +63,28 @@ def guard(charm: 'CharmBase',
|
|||||||
yield
|
yield
|
||||||
logging.info("Completed guarded section fully: '%s'", section)
|
logging.info("Completed guarded section fully: '%s'", section)
|
||||||
except GuardException as e:
|
except GuardException as e:
|
||||||
logger.info("Guarded Section: Early exit from '%s' due to '%s'.",
|
logger.info(
|
||||||
section, str(e))
|
"Guarded Section: Early exit from '%s' due to '%s'.",
|
||||||
|
section,
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
except BlockedException as e:
|
except BlockedException 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)
|
||||||
|
)
|
||||||
charm.unit.status = BlockedStatus(e.msg)
|
charm.unit.status = BlockedStatus(e.msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# something else went wrong
|
# something else went wrong
|
||||||
if handle_exception:
|
if handle_exception:
|
||||||
logging.error("Exception raised in secion '%s': %s",
|
logging.error(
|
||||||
section, str(e))
|
"Exception raised in secion '%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.unit.status = BlockedStatus(
|
charm.unit.status = BlockedStatus(
|
||||||
"Error in charm (see logs): {}".format(str(e)))
|
"Error in charm (see logs): {}".format(str(e))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
# Copyright 2021 Canonical Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Common interfaces not charm specific."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import ops.model
|
||||||
|
|
||||||
from ops.framework import EventBase
|
from ops.framework import EventBase
|
||||||
from ops.framework import EventSource
|
from ops.framework import EventSource
|
||||||
from ops.framework import Object
|
from ops.framework import Object
|
||||||
@ -11,80 +27,76 @@ from ops.framework import StoredState
|
|||||||
|
|
||||||
|
|
||||||
class PeersRelationCreatedEvent(EventBase):
|
class PeersRelationCreatedEvent(EventBase):
|
||||||
"""
|
"""The PeersRelationCreatedEvent indicates that the peer relation now exists.
|
||||||
The PeersRelationCreatedEvent indicates that the peer relation now exists.
|
|
||||||
It does not indicate that any peers are available or have joined, simply
|
It does not indicate that any peers are available or have joined, simply
|
||||||
that the relation exists. This is useful to to indicate that the
|
that the relation exists. This is useful to to indicate that the
|
||||||
application databag is available for storing information shared across
|
application databag is available for storing information shared across
|
||||||
units.
|
units.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PeersDataChangedEvent(EventBase):
|
class PeersDataChangedEvent(EventBase):
|
||||||
"""
|
"""The PeersDataChangedEvent indicates peer data hjas changed."""
|
||||||
The CharmPasswordChangedEvent indicates that the leader unit has changed
|
|
||||||
the password that the charm administrator uses.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PeersEvents(ObjectEvents):
|
class PeersEvents(ObjectEvents):
|
||||||
|
"""Peer Events."""
|
||||||
|
|
||||||
peers_relation_created = EventSource(PeersRelationCreatedEvent)
|
peers_relation_created = EventSource(PeersRelationCreatedEvent)
|
||||||
peers_data_changed = EventSource(PeersDataChangedEvent)
|
peers_data_changed = EventSource(PeersDataChangedEvent)
|
||||||
|
|
||||||
|
|
||||||
class OperatorPeers(Object):
|
class OperatorPeers(Object):
|
||||||
|
"""Interface for the peers relation."""
|
||||||
|
|
||||||
on = PeersEvents()
|
on = PeersEvents()
|
||||||
state = StoredState()
|
state = StoredState()
|
||||||
|
|
||||||
def __init__(self, charm, relation_name):
|
def __init__(self, charm: ops.charm.CharmBase, relation_name: str) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
super().__init__(charm, relation_name)
|
super().__init__(charm, relation_name)
|
||||||
self.relation_name = relation_name
|
self.relation_name = relation_name
|
||||||
self.framework.observe(
|
self.framework.observe(
|
||||||
charm.on[relation_name].relation_created,
|
charm.on[relation_name].relation_created, self.on_created
|
||||||
self.on_created
|
|
||||||
)
|
)
|
||||||
self.framework.observe(
|
self.framework.observe(
|
||||||
charm.on[relation_name].relation_changed,
|
charm.on[relation_name].relation_changed, self.on_changed
|
||||||
self.on_changed
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def peers_rel(self):
|
def peers_rel(self) -> ops.model.Relation:
|
||||||
|
"""Peer relation."""
|
||||||
return self.framework.model.get_relation(self.relation_name)
|
return self.framework.model.get_relation(self.relation_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _app_data_bag(self) -> typing.Dict[str, str]:
|
def _app_data_bag(self) -> typing.Dict[str, str]:
|
||||||
"""
|
"""Return all app data on peer relation."""
|
||||||
|
|
||||||
"""
|
|
||||||
return self.peers_rel.data[self.peers_rel.app]
|
return self.peers_rel.data[self.peers_rel.app]
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event: ops.framework.EventBase) -> None:
|
||||||
logging.info('Peers on_created')
|
"""Handle relation created event."""
|
||||||
|
logging.info("Peers on_created")
|
||||||
self.on.peers_relation_created.emit()
|
self.on.peers_relation_created.emit()
|
||||||
|
|
||||||
def on_changed(self, event):
|
def on_changed(self, event: ops.framework.EventBase) -> None:
|
||||||
logging.info('Peers on_changed')
|
"""Handle relation changed event."""
|
||||||
|
logging.info("Peers on_changed")
|
||||||
self.on.peers_data_changed.emit()
|
self.on.peers_data_changed.emit()
|
||||||
|
|
||||||
def set_app_data(self, settings) -> None:
|
def set_app_data(self, settings: typing.Dict[str, str]) -> None:
|
||||||
"""
|
"""Publish settings on the peer app data bag."""
|
||||||
|
|
||||||
"""
|
|
||||||
for k, v in settings.items():
|
for k, v in settings.items():
|
||||||
self._app_data_bag[k] = v
|
self._app_data_bag[k] = v
|
||||||
|
|
||||||
def get_app_data(self, key) -> None:
|
def get_app_data(self, key: str) -> None:
|
||||||
"""
|
"""Get the value corresponding to key from the app data bag."""
|
||||||
|
|
||||||
"""
|
|
||||||
return self._app_data_bag.get(key)
|
return self._app_data_bag.get(key)
|
||||||
|
|
||||||
def get_all_app_data(self) -> None:
|
def get_all_app_data(self) -> None:
|
||||||
"""
|
"""Return all the app data from the relation."""
|
||||||
|
|
||||||
"""
|
|
||||||
return self._app_data_bag
|
return self._app_data_bag
|
||||||
|
@ -12,16 +12,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Base classes for defining a charm using the Operator framework.
|
"""Base classes for defining a charm using the Operator framework."""
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from typing import Callable, List, Tuple
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import ops.charm
|
import ops.charm
|
||||||
|
import ops.framework
|
||||||
|
|
||||||
import charms.nginx_ingress_integrator.v0.ingress as ingress
|
import charms.nginx_ingress_integrator.v0.ingress as ingress
|
||||||
import charms.sunbeam_mysql_k8s.v0.mysql as mysql
|
import charms.sunbeam_mysql_k8s.v0.mysql as mysql
|
||||||
@ -33,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RelationHandler(ops.charm.Object):
|
class RelationHandler(ops.charm.Object):
|
||||||
"""Base handler class for relations
|
"""Base handler class for relations.
|
||||||
|
|
||||||
A relation handler is used to manage a charms interaction with a relation
|
A relation handler is used to manage a charms interaction with a relation
|
||||||
interface. This includes:
|
interface. This includes:
|
||||||
@ -46,8 +44,13 @@ class RelationHandler(ops.charm.Object):
|
|||||||
recieved and sent on an interface.
|
recieved and sent on an interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, charm: ops.charm.CharmBase,
|
def __init__(
|
||||||
relation_name: str, callback_f: Callable):
|
self,
|
||||||
|
charm: ops.charm.CharmBase,
|
||||||
|
relation_name: str,
|
||||||
|
callback_f: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
super().__init__(charm, None)
|
super().__init__(charm, None)
|
||||||
self.charm = charm
|
self.charm = charm
|
||||||
self.relation_name = relation_name
|
self.relation_name = relation_name
|
||||||
@ -63,21 +66,25 @@ class RelationHandler(ops.charm.Object):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_interface(self) -> Tuple[ops.charm.Object, str]:
|
def get_interface(self) -> Tuple[ops.charm.Object, str]:
|
||||||
"""Returns the interface that this handler encapsulates.
|
"""Return the interface that this handler encapsulates.
|
||||||
|
|
||||||
This is a combination of the interface object and the
|
This is a combination of the interface object and the
|
||||||
name of the relation its wired into.
|
name of the relation its wired into.
|
||||||
"""
|
"""
|
||||||
return self.interface, self.relation_name
|
return self.interface, self.relation_name
|
||||||
|
|
||||||
def interface_properties(self):
|
def interface_properties(self) -> dict:
|
||||||
|
"""Extract properties of the interface."""
|
||||||
property_names = [
|
property_names = [
|
||||||
p for p in dir(self.interface) if isinstance(
|
p
|
||||||
getattr(type(self.interface), p, None), property)]
|
for p in dir(self.interface)
|
||||||
|
if isinstance(getattr(type(self.interface), p, None), property)
|
||||||
|
]
|
||||||
properties = {
|
properties = {
|
||||||
p: getattr(self.interface, p)
|
p: getattr(self.interface, p)
|
||||||
for p in property_names
|
for p in property_names
|
||||||
if not p.startswith('_') and p not in ['model']}
|
if not p.startswith("_") and p not in ["model"]
|
||||||
|
}
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -91,23 +98,25 @@ class RelationHandler(ops.charm.Object):
|
|||||||
|
|
||||||
|
|
||||||
class IngressHandler(RelationHandler):
|
class IngressHandler(RelationHandler):
|
||||||
"""Handler for Ingress relations"""
|
"""Handler for Ingress relations."""
|
||||||
|
|
||||||
def __init__(self, charm: ops.charm.CharmBase,
|
def __init__(
|
||||||
relation_name: str,
|
self,
|
||||||
service_name: str,
|
charm: ops.charm.CharmBase,
|
||||||
default_public_ingress_port: int,
|
relation_name: str,
|
||||||
callback_f: Callable):
|
service_name: str,
|
||||||
|
default_public_ingress_port: int,
|
||||||
|
callback_f: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.default_public_ingress_port = default_public_ingress_port
|
self.default_public_ingress_port = default_public_ingress_port
|
||||||
self.service_name = service_name
|
self.service_name = service_name
|
||||||
super().__init__(charm, relation_name, callback_f)
|
super().__init__(charm, relation_name, callback_f)
|
||||||
|
|
||||||
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")
|
||||||
interface = ingress.IngressRequires(
|
interface = ingress.IngressRequires(self.charm, self.ingress_config)
|
||||||
self.charm,
|
|
||||||
self.ingress_config)
|
|
||||||
return interface
|
return interface
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -116,75 +125,78 @@ class IngressHandler(RelationHandler):
|
|||||||
# Most charms probably won't (or shouldn't) expose service-port
|
# Most charms probably won't (or shouldn't) expose service-port
|
||||||
# but use it if its there.
|
# but use it if its there.
|
||||||
port = self.model.config.get(
|
port = self.model.config.get(
|
||||||
'service-port',
|
"service-port", self.default_public_ingress_port
|
||||||
self.default_public_ingress_port)
|
)
|
||||||
svc_hostname = self.model.config.get(
|
svc_hostname = self.model.config.get(
|
||||||
'os-public-hostname',
|
"os-public-hostname", self.service_name
|
||||||
self.service_name)
|
)
|
||||||
return {
|
return {
|
||||||
'service-hostname': svc_hostname,
|
"service-hostname": svc_hostname,
|
||||||
'service-name': self.charm.app.name,
|
"service-name": self.charm.app.name,
|
||||||
'service-port': port}
|
"service-port": port,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
|
"""Whether the handler is ready for use."""
|
||||||
# Nothing to wait for
|
# Nothing to wait for
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def context(self):
|
def context(self) -> dict:
|
||||||
|
"""Context containing ingress data."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class DBHandler(RelationHandler):
|
class DBHandler(RelationHandler):
|
||||||
"""Handler for DB relations"""
|
"""Handler for DB relations."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
charm: ops.charm.CharmBase,
|
charm: ops.charm.CharmBase,
|
||||||
relation_name: str,
|
relation_name: str,
|
||||||
callback_f,
|
callback_f: Callable,
|
||||||
databases=None
|
databases: List[str] = None,
|
||||||
):
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.databases = databases
|
self.databases = databases
|
||||||
super().__init__(charm, relation_name, callback_f)
|
super().__init__(charm, relation_name, callback_f)
|
||||||
|
|
||||||
def setup_event_handler(self) -> ops.charm.Object:
|
def setup_event_handler(self) -> ops.charm.Object:
|
||||||
"""Configure event handlers for a MySQL relation."""
|
"""Configure event handlers for a MySQL relation."""
|
||||||
logger.debug('Setting up DB event handler')
|
logger.debug("Setting up DB event handler")
|
||||||
db = mysql.MySQLConsumer(
|
db = mysql.MySQLConsumer(
|
||||||
self.charm,
|
self.charm, self.relation_name, databases=self.databases
|
||||||
self.relation_name,
|
)
|
||||||
databases=self.databases)
|
_rname = self.relation_name.replace("-", "_")
|
||||||
_rname = self.relation_name.replace('-', '_')
|
|
||||||
db_relation_event = getattr(
|
db_relation_event = getattr(
|
||||||
self.charm.on,
|
self.charm.on, f"{_rname}_relation_changed"
|
||||||
f'{_rname}_relation_changed')
|
)
|
||||||
self.framework.observe(db_relation_event,
|
self.framework.observe(db_relation_event, self._on_database_changed)
|
||||||
self._on_database_changed)
|
|
||||||
return db
|
return db
|
||||||
|
|
||||||
def _on_database_changed(self, event) -> None:
|
def _on_database_changed(self, event: ops.framework.EventBase) -> None:
|
||||||
"""Handles database change events."""
|
"""Handle database change events."""
|
||||||
databases = self.interface.databases()
|
databases = self.interface.databases()
|
||||||
logger.info(f'Received databases: {databases}')
|
logger.info(f"Received databases: {databases}")
|
||||||
|
|
||||||
if not databases:
|
if not databases:
|
||||||
return
|
return
|
||||||
credentials = self.interface.credentials()
|
credentials = self.interface.credentials()
|
||||||
# XXX Lets not log the credentials
|
# XXX Lets not log the credentials
|
||||||
logger.info(f'Received credentials: {credentials}')
|
logger.info(f"Received credentials: {credentials}")
|
||||||
self.callback_f(event)
|
self.callback_f(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Handler ready for use."""
|
"""Whether the handler is ready for use."""
|
||||||
try:
|
try:
|
||||||
# Nothing to wait for
|
# Nothing to wait for
|
||||||
return bool(self.interface.databases())
|
return bool(self.interface.databases())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def context(self):
|
def context(self) -> dict:
|
||||||
|
"""Context containing database connection data."""
|
||||||
try:
|
try:
|
||||||
databases = self.interface.databases()
|
databases = self.interface.databases()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -192,15 +204,17 @@ class DBHandler(RelationHandler):
|
|||||||
if not databases:
|
if not databases:
|
||||||
return {}
|
return {}
|
||||||
ctxt = {
|
ctxt = {
|
||||||
'database': self.interface.databases()[0],
|
"database": self.interface.databases()[0],
|
||||||
'database_host': self.interface.credentials().get('address'),
|
"database_host": self.interface.credentials().get("address"),
|
||||||
'database_password': self.interface.credentials().get('password'),
|
"database_password": self.interface.credentials().get("password"),
|
||||||
'database_user': self.interface.credentials().get('username'),
|
"database_user": self.interface.credentials().get("username"),
|
||||||
'database_type': 'mysql+pymysql'}
|
"database_type": "mysql+pymysql",
|
||||||
|
}
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
class AMQPHandler(RelationHandler):
|
class AMQPHandler(RelationHandler):
|
||||||
|
"""Handler for managing a amqp relation."""
|
||||||
|
|
||||||
DEFAULT_PORT = "5672"
|
DEFAULT_PORT = "5672"
|
||||||
|
|
||||||
@ -208,15 +222,16 @@ class AMQPHandler(RelationHandler):
|
|||||||
self,
|
self,
|
||||||
charm: ops.charm.CharmBase,
|
charm: ops.charm.CharmBase,
|
||||||
relation_name: str,
|
relation_name: str,
|
||||||
callback_f,
|
callback_f: Callable,
|
||||||
username: str,
|
username: str,
|
||||||
vhost: int,
|
vhost: int,
|
||||||
):
|
) -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.username = username
|
self.username = username
|
||||||
self.vhost = vhost
|
self.vhost = vhost
|
||||||
super().__init__(charm, relation_name, callback_f)
|
super().__init__(charm, relation_name, callback_f)
|
||||||
|
|
||||||
def setup_event_handler(self):
|
def setup_event_handler(self) -> ops.charm.Object:
|
||||||
"""Configure event handlers for an AMQP relation."""
|
"""Configure event handlers for an AMQP relation."""
|
||||||
logger.debug("Setting up AMQP event handler")
|
logger.debug("Setting up AMQP event handler")
|
||||||
amqp = sunbeam_amqp.AMQPRequires(
|
amqp = sunbeam_amqp.AMQPRequires(
|
||||||
@ -225,21 +240,22 @@ class AMQPHandler(RelationHandler):
|
|||||||
self.framework.observe(amqp.on.ready, self._on_amqp_ready)
|
self.framework.observe(amqp.on.ready, self._on_amqp_ready)
|
||||||
return amqp
|
return amqp
|
||||||
|
|
||||||
def _on_amqp_ready(self, event) -> None:
|
def _on_amqp_ready(self, event: ops.framework.EventBase) -> None:
|
||||||
"""Handles AMQP change events."""
|
"""Handle AMQP change events."""
|
||||||
# Ready is only emitted when the interface considers
|
# Ready is only emitted when the interface considers
|
||||||
# that the relation is complete (indicated by a password)
|
# that the relation is complete (indicated by a password)
|
||||||
self.callback_f(event)
|
self.callback_f(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Handler ready for use."""
|
"""Whether handler is ready for use."""
|
||||||
try:
|
try:
|
||||||
return bool(self.interface.password)
|
return bool(self.interface.password)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def context(self):
|
def context(self) -> dict:
|
||||||
|
"""Context containing AMQP connection data."""
|
||||||
try:
|
try:
|
||||||
hosts = self.interface.hostnames
|
hosts = self.interface.hostnames
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -247,60 +263,65 @@ class AMQPHandler(RelationHandler):
|
|||||||
if not hosts:
|
if not hosts:
|
||||||
return {}
|
return {}
|
||||||
ctxt = super().context()
|
ctxt = super().context()
|
||||||
ctxt['hostnames'] = list(set(ctxt['hostnames']))
|
ctxt["hostnames"] = list(set(ctxt["hostnames"]))
|
||||||
ctxt['hosts'] = ','.join(ctxt['hostnames'])
|
ctxt["hosts"] = ",".join(ctxt["hostnames"])
|
||||||
ctxt['port'] = ctxt.get('ssl_port') or self.DEFAULT_PORT
|
ctxt["port"] = ctxt.get("ssl_port") or self.DEFAULT_PORT
|
||||||
transport_url_hosts = ','.join([
|
transport_url_hosts = ",".join(
|
||||||
"{}:{}@{}:{}".format(self.username,
|
[
|
||||||
ctxt['password'],
|
"{}:{}@{}:{}".format(
|
||||||
host_, # TODO deal with IPv6
|
self.username,
|
||||||
ctxt['port'])
|
ctxt["password"],
|
||||||
for host_ in ctxt['hostnames']
|
host_, # TODO deal with IPv6
|
||||||
])
|
ctxt["port"],
|
||||||
|
)
|
||||||
|
for host_ in ctxt["hostnames"]
|
||||||
|
]
|
||||||
|
)
|
||||||
transport_url = "rabbit://{}/{}".format(
|
transport_url = "rabbit://{}/{}".format(
|
||||||
transport_url_hosts,
|
transport_url_hosts, self.vhost
|
||||||
self.vhost)
|
)
|
||||||
ctxt['transport_url'] = transport_url
|
ctxt["transport_url"] = transport_url
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
class IdentityServiceRequiresHandler(RelationHandler):
|
class IdentityServiceRequiresHandler(RelationHandler):
|
||||||
|
"""Handler for managing a identity-service relation."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
charm: ops.charm.CharmBase,
|
charm: ops.charm.CharmBase,
|
||||||
relation_name: str,
|
relation_name: str,
|
||||||
callback_f,
|
callback_f: Callable,
|
||||||
service_endpoints: dict,
|
service_endpoints: dict,
|
||||||
region: str,
|
region: str,
|
||||||
):
|
) -> None:
|
||||||
|
"""Ron constructor."""
|
||||||
self.service_endpoints = service_endpoints
|
self.service_endpoints = service_endpoints
|
||||||
self.region = region
|
self.region = region
|
||||||
super().__init__(charm, relation_name, callback_f)
|
super().__init__(charm, relation_name, callback_f)
|
||||||
|
|
||||||
def setup_event_handler(self):
|
def setup_event_handler(self) -> ops.charm.Object:
|
||||||
"""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")
|
||||||
id_svc = sunbeam_id_svc.IdentityServiceRequires(
|
id_svc = sunbeam_id_svc.IdentityServiceRequires(
|
||||||
self.charm,
|
self.charm, self.relation_name, self.service_endpoints, self.region
|
||||||
self.relation_name,
|
|
||||||
self.service_endpoints,
|
|
||||||
self.region
|
|
||||||
)
|
)
|
||||||
self.framework.observe(
|
self.framework.observe(
|
||||||
id_svc.on.ready,
|
id_svc.on.ready, self._on_identity_service_ready
|
||||||
self._on_identity_service_ready)
|
)
|
||||||
return id_svc
|
return id_svc
|
||||||
|
|
||||||
def _on_identity_service_ready(self, event) -> None:
|
def _on_identity_service_ready(
|
||||||
"""Handles AMQP change events."""
|
self, event: ops.framework.EventBase
|
||||||
|
) -> None:
|
||||||
|
"""Handle AMQP change events."""
|
||||||
# Ready is only emitted when the interface considers
|
# Ready is only emitted when the interface considers
|
||||||
# that the relation is complete (indicated by a password)
|
# that the relation is complete (indicated by a password)
|
||||||
self.callback_f(event)
|
self.callback_f(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Handler ready for use."""
|
"""Whether handler is ready for use."""
|
||||||
try:
|
try:
|
||||||
return bool(self.interface.service_password)
|
return bool(self.interface.service_password)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -308,10 +329,11 @@ class IdentityServiceRequiresHandler(RelationHandler):
|
|||||||
|
|
||||||
|
|
||||||
class BasePeerHandler(RelationHandler):
|
class BasePeerHandler(RelationHandler):
|
||||||
|
"""Base handler for managing a peers relation."""
|
||||||
|
|
||||||
LEADER_READY_KEY = 'leader_ready'
|
LEADER_READY_KEY = "leader_ready"
|
||||||
|
|
||||||
def setup_event_handler(self):
|
def setup_event_handler(self) -> None:
|
||||||
"""Configure event handlers for peer relation."""
|
"""Configure event handlers for peer relation."""
|
||||||
logger.debug("Setting up peer event handler")
|
logger.debug("Setting up peer event handler")
|
||||||
peer_int = sunbeam_interfaces.OperatorPeers(
|
peer_int = sunbeam_interfaces.OperatorPeers(
|
||||||
@ -319,39 +341,50 @@ class BasePeerHandler(RelationHandler):
|
|||||||
self.relation_name,
|
self.relation_name,
|
||||||
)
|
)
|
||||||
self.framework.observe(
|
self.framework.observe(
|
||||||
peer_int.on.peers_data_changed,
|
peer_int.on.peers_data_changed, self._on_peers_data_changed
|
||||||
self._on_peers_data_changed)
|
)
|
||||||
return peer_int
|
return peer_int
|
||||||
|
|
||||||
def _on_peers_data_changed(self, event) -> None:
|
def _on_peers_data_changed(self, event: ops.framework.EventBase) -> None:
|
||||||
|
"""Process peer data changed event."""
|
||||||
self.callback_f(event)
|
self.callback_f(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
|
"""Whether the handler is complete."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def context(self):
|
def context(self) -> dict:
|
||||||
|
"""Return all app data set on the peer relation."""
|
||||||
try:
|
try:
|
||||||
return self.interface.get_all_app_data()
|
return self.interface.get_all_app_data()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def set_app_data(self, settings):
|
def set_app_data(self, settings: dict) -> None:
|
||||||
|
"""Store data in peer app db."""
|
||||||
self.interface.set_app_data(settings)
|
self.interface.set_app_data(settings)
|
||||||
|
|
||||||
def get_app_data(self, key):
|
def get_app_data(self, key: str) -> str:
|
||||||
|
"""Retrieve data from the peer relation."""
|
||||||
return self.interface.get_app_data(key)
|
return self.interface.get_app_data(key)
|
||||||
|
|
||||||
def leader_set(self, settings=None, **kwargs):
|
def leader_get(self, key: str) -> str:
|
||||||
"""Juju leader set value(s)"""
|
"""Retrieve data from the peer relation."""
|
||||||
|
return self.peers.get_app_data(key)
|
||||||
|
|
||||||
|
def leader_set(self, settings: dict, **kwargs) -> None:
|
||||||
|
"""Store data in peer app db."""
|
||||||
settings = settings or {}
|
settings = settings or {}
|
||||||
settings.update(kwargs)
|
settings.update(kwargs)
|
||||||
self.set_app_data(settings)
|
self.set_app_data(settings)
|
||||||
|
|
||||||
def set_leader_ready(self):
|
def set_leader_ready(self) -> None:
|
||||||
|
"""Tell peers the leader is ready."""
|
||||||
self.set_app_data({self.LEADER_READY_KEY: json.dumps(True)})
|
self.set_app_data({self.LEADER_READY_KEY: json.dumps(True)})
|
||||||
|
|
||||||
def is_leader_ready(self):
|
def is_leader_ready(self) -> bool:
|
||||||
|
"""Whether the leader has announced it is ready."""
|
||||||
ready = self.get_app_data(self.LEADER_READY_KEY)
|
ready = self.get_app_data(self.LEADER_READY_KEY)
|
||||||
if ready is None:
|
if ready is None:
|
||||||
return False
|
return False
|
||||||
|
@ -12,22 +12,26 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Module for rendering templates inside containers."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
from collections import defaultdict
|
if TYPE_CHECKING:
|
||||||
|
import advanced_sunbeam_openstack.core as sunbeam_core
|
||||||
|
import ops.model
|
||||||
|
|
||||||
from charmhelpers.contrib.openstack.templating import (
|
from charmhelpers.contrib.openstack.templating import get_loader
|
||||||
OSConfigException,
|
|
||||||
OSConfigRenderer,
|
|
||||||
get_loader)
|
|
||||||
import jinja2
|
import jinja2
|
||||||
# from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_container(containers, name):
|
def get_container(
|
||||||
|
containers: List['ops.model.Container'], name: str
|
||||||
|
) -> 'ops.model.Container':
|
||||||
|
"""Search for container with given name inlist of containers."""
|
||||||
container = None
|
container = None
|
||||||
for c in containers:
|
for c in containers:
|
||||||
if c.name == name:
|
if c.name == name:
|
||||||
@ -35,113 +39,31 @@ def get_container(containers, name):
|
|||||||
return container
|
return container
|
||||||
|
|
||||||
|
|
||||||
def sidecar_config_render(containers, container_configs, template_dir,
|
def sidecar_config_render(
|
||||||
openstack_release, adapters):
|
containers: List['ops.model.Container'],
|
||||||
|
container_configs: List['sunbeam_core.ContainerConfigFile'],
|
||||||
|
template_dir: str,
|
||||||
|
openstack_release: str,
|
||||||
|
context: 'sunbeam_core.OPSCharmContexts',
|
||||||
|
) -> None:
|
||||||
|
"""Render templates inside containers."""
|
||||||
loader = get_loader(template_dir, openstack_release)
|
loader = get_loader(template_dir, openstack_release)
|
||||||
_tmpl_env = jinja2.Environment(loader=loader)
|
_tmpl_env = jinja2.Environment(loader=loader)
|
||||||
for config in container_configs:
|
for config in container_configs:
|
||||||
for container_name in config.container_names:
|
for container_name in config.container_names:
|
||||||
try:
|
try:
|
||||||
template = _tmpl_env.get_template(
|
template = _tmpl_env.get_template(
|
||||||
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)
|
||||||
|
)
|
||||||
container = get_container(containers, container_name)
|
container = get_container(containers, container_name)
|
||||||
contents = template.render(adapters)
|
contents = template.render(context)
|
||||||
kwargs = {
|
kwargs = {"user": config.user, "group": config.group}
|
||||||
'user': config.user,
|
|
||||||
'group': config.group}
|
|
||||||
container.push(config.path, contents, **kwargs)
|
container.push(config.path, contents, **kwargs)
|
||||||
log.debug(f'Wrote template {config.path} in container '
|
log.debug(
|
||||||
f'{container.name}.')
|
f"Wrote template {config.path} in container "
|
||||||
|
f"{container.name}."
|
||||||
|
)
|
||||||
class SidecarConfigRenderer(OSConfigRenderer):
|
|
||||||
|
|
||||||
"""
|
|
||||||
This class provides a common templating system to be used by OpenStack
|
|
||||||
sidecar charms.
|
|
||||||
"""
|
|
||||||
def __init__(self, templates_dir, openstack_release):
|
|
||||||
super(SidecarConfigRenderer, self).__init__(templates_dir,
|
|
||||||
openstack_release)
|
|
||||||
|
|
||||||
|
|
||||||
class _SidecarConfigRenderer(OSConfigRenderer):
|
|
||||||
|
|
||||||
"""
|
|
||||||
This class provides a common templating system to be used by OpenStack
|
|
||||||
sidecar charms.
|
|
||||||
"""
|
|
||||||
def __init__(self, templates_dir, openstack_release):
|
|
||||||
super(SidecarConfigRenderer, self).__init__(templates_dir,
|
|
||||||
openstack_release)
|
|
||||||
self.config_to_containers = defaultdict(set)
|
|
||||||
self.owner_info = defaultdict(set)
|
|
||||||
|
|
||||||
def _get_template(self, template):
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._get_tmpl_env()
|
|
||||||
if not template.endswith('.j2'):
|
|
||||||
template += '.j2'
|
|
||||||
template = self._tmpl_env.get_template(template)
|
|
||||||
log.debug(f'Loaded template from {template.filename}')
|
|
||||||
return template
|
|
||||||
|
|
||||||
def register(self, config_file, contexts, config_template=None,
|
|
||||||
containers=None, user=None, group=None):
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
# NOTE(wolsen): Intentionally overriding base class to raise an error
|
|
||||||
# if this is accidentally used instead.
|
|
||||||
if containers is None:
|
|
||||||
raise ValueError('One or more containers must be provided')
|
|
||||||
|
|
||||||
super().register(config_file, contexts, config_template)
|
|
||||||
|
|
||||||
# Register user/group info. There's a better way to do this for sure
|
|
||||||
if user or group:
|
|
||||||
self.owner_info[config_file] = (user, group)
|
|
||||||
|
|
||||||
for container in containers:
|
|
||||||
self.config_to_containers[config_file].add(container)
|
|
||||||
log.debug(f'Registered config file "{config_file}" for container '
|
|
||||||
f'{container}')
|
|
||||||
|
|
||||||
def write(self, config_file, container):
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
containers = self.config_to_containers.get(config_file)
|
|
||||||
if not containers or container.name not in containers:
|
|
||||||
log.error(f'Config file {config_file} not registered for '
|
|
||||||
f'container {container.name}')
|
|
||||||
raise OSConfigException
|
|
||||||
|
|
||||||
contents = self.render(config_file)
|
|
||||||
owner_info = self.owner_info.get(config_file)
|
|
||||||
kwargs = {}
|
|
||||||
log.debug(f'Got owner_info of {owner_info}')
|
|
||||||
if owner_info:
|
|
||||||
user, group = owner_info
|
|
||||||
kwargs['user'] = user
|
|
||||||
kwargs['group'] = group
|
|
||||||
container.push(config_file, contents, **kwargs)
|
|
||||||
|
|
||||||
log.debug(f'Wrote template {config_file} in container '
|
|
||||||
f'{container.name}.')
|
|
||||||
|
|
||||||
def write_all(self, container=None):
|
|
||||||
for config_file, containers in self.config_to_containers.items():
|
|
||||||
if container:
|
|
||||||
if container.name not in containers:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.write(config_file, container)
|
|
||||||
else:
|
|
||||||
for c in containers:
|
|
||||||
self.write(config_file, c)
|
|
||||||
|
@ -14,20 +14,23 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Module containing shared code to be used in a charms units tests."""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import ops
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
import unittest
|
import unittest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from mock import patch
|
from mock import 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
|
||||||
|
|
||||||
@ -35,206 +38,233 @@ from ops.testing import Harness, _TestingModelBackend, _TestingPebbleClient
|
|||||||
|
|
||||||
|
|
||||||
class CharmTestCase(unittest.TestCase):
|
class CharmTestCase(unittest.TestCase):
|
||||||
|
"""Class to make mocking easier."""
|
||||||
|
|
||||||
def setUp(self, obj, patches):
|
def setUp(self, obj: 'typing.ANY', patches: 'typing.List') -> None:
|
||||||
|
"""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):
|
def patch(self, method: 'typing.ANY') -> Mock:
|
||||||
|
"""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, method):
|
def patch_obj(self, obj: 'typing.ANY', method: 'typing.ANY') -> Mock:
|
||||||
|
"""Patch the named method on obj."""
|
||||||
_m = patch.object(obj, method)
|
_m = patch.object(obj, method)
|
||||||
mock = _m.start()
|
mock = _m.start()
|
||||||
self.addCleanup(_m.stop)
|
self.addCleanup(_m.stop)
|
||||||
return mock
|
return mock
|
||||||
|
|
||||||
def patch_all(self):
|
def patch_all(self) -> None:
|
||||||
|
"""Patch all objects in self.patches."""
|
||||||
for method in self.patches:
|
for method in self.patches:
|
||||||
setattr(self, method, self.patch(method))
|
setattr(self, method, self.patch(method))
|
||||||
|
|
||||||
|
|
||||||
def add_base_amqp_relation(harness):
|
def add_base_amqp_relation(harness: Harness) -> str:
|
||||||
rel_id = harness.add_relation('amqp', 'rabbitmq')
|
"""Add amqp relation."""
|
||||||
harness.add_relation_unit(
|
rel_id = harness.add_relation("amqp", "rabbitmq")
|
||||||
rel_id,
|
harness.add_relation_unit(rel_id, "rabbitmq/0")
|
||||||
'rabbitmq/0')
|
harness.add_relation_unit(rel_id, "rabbitmq/0")
|
||||||
harness.add_relation_unit(
|
|
||||||
rel_id,
|
|
||||||
'rabbitmq/0')
|
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id, "rabbitmq/0", {"ingress-address": "10.0.0.13"}
|
||||||
'rabbitmq/0',
|
)
|
||||||
{'ingress-address': '10.0.0.13'})
|
|
||||||
return rel_id
|
return rel_id
|
||||||
|
|
||||||
|
|
||||||
def add_amqp_relation_credentials(harness, rel_id):
|
def add_amqp_relation_credentials(
|
||||||
|
harness: Harness, rel_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Add amqp data to amqp relation."""
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id,
|
||||||
'rabbitmq',
|
"rabbitmq",
|
||||||
{
|
{"hostname": "rabbithost1.local", "password": "rabbit.pass"},
|
||||||
'hostname': 'rabbithost1.local',
|
)
|
||||||
'password': 'rabbit.pass'})
|
|
||||||
|
|
||||||
|
|
||||||
def add_base_identity_service_relation(harness):
|
def add_base_identity_service_relation(harness: Harness) -> str:
|
||||||
rel_id = harness.add_relation('identity-service', 'keystone')
|
"""Add identity-service relation."""
|
||||||
harness.add_relation_unit(
|
rel_id = harness.add_relation("identity-service", "keystone")
|
||||||
rel_id,
|
harness.add_relation_unit(rel_id, "keystone/0")
|
||||||
'keystone/0')
|
harness.add_relation_unit(rel_id, "keystone/0")
|
||||||
harness.add_relation_unit(
|
|
||||||
rel_id,
|
|
||||||
'keystone/0')
|
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id, "keystone/0", {"ingress-address": "10.0.0.33"}
|
||||||
'keystone/0',
|
)
|
||||||
{'ingress-address': '10.0.0.33'})
|
|
||||||
return rel_id
|
return rel_id
|
||||||
|
|
||||||
|
|
||||||
def add_identity_service_relation_response(harness, rel_id):
|
def add_identity_service_relation_response(
|
||||||
|
harness: Harness, rel_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Add id service data to identity-service relation."""
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id,
|
||||||
'keystone',
|
"keystone",
|
||||||
{
|
{
|
||||||
'admin-domain-id': 'admindomid1',
|
"admin-domain-id": "admindomid1",
|
||||||
'admin-project-id': 'adminprojid1',
|
"admin-project-id": "adminprojid1",
|
||||||
'admin-user-id': 'adminuserid1',
|
"admin-user-id": "adminuserid1",
|
||||||
'api-version': '3',
|
"api-version": "3",
|
||||||
'auth-host': 'keystone.local',
|
"auth-host": "keystone.local",
|
||||||
'auth-port': '12345',
|
"auth-port": "12345",
|
||||||
'auth-protocol': 'http',
|
"auth-protocol": "http",
|
||||||
'internal-host': 'keystone.internal',
|
"internal-host": "keystone.internal",
|
||||||
'internal-port': '5000',
|
"internal-port": "5000",
|
||||||
'internal-protocol': 'http',
|
"internal-protocol": "http",
|
||||||
'service-domain': 'servicedom',
|
"service-domain": "servicedom",
|
||||||
'service-domain_id': 'svcdomid1',
|
"service-domain_id": "svcdomid1",
|
||||||
'service-host': 'keystone.service',
|
"service-host": "keystone.service",
|
||||||
'service-password': 'svcpass1',
|
"service-password": "svcpass1",
|
||||||
'service-port': '5000',
|
"service-port": "5000",
|
||||||
'service-protocol': 'http',
|
"service-protocol": "http",
|
||||||
'service-project': 'svcproj1',
|
"service-project": "svcproj1",
|
||||||
'service-project-id': 'svcprojid1',
|
"service-project-id": "svcprojid1",
|
||||||
'service-username': 'svcuser1'})
|
"service-username": "svcuser1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_base_db_relation(harness):
|
def add_base_db_relation(harness: Harness) -> str:
|
||||||
rel_id = harness.add_relation('shared-db', 'mysql')
|
"""Add db relation."""
|
||||||
harness.add_relation_unit(
|
rel_id = harness.add_relation("shared-db", "mysql")
|
||||||
rel_id,
|
harness.add_relation_unit(rel_id, "mysql/0")
|
||||||
'mysql/0')
|
harness.add_relation_unit(rel_id, "mysql/0")
|
||||||
harness.add_relation_unit(
|
|
||||||
rel_id,
|
|
||||||
'mysql/0')
|
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id, "mysql/0", {"ingress-address": "10.0.0.3"}
|
||||||
'mysql/0',
|
)
|
||||||
{'ingress-address': '10.0.0.3'})
|
|
||||||
return rel_id
|
return rel_id
|
||||||
|
|
||||||
|
|
||||||
def add_db_relation_credentials(harness, rel_id):
|
def add_db_relation_credentials(
|
||||||
|
harness: Harness, rel_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Add db credentials data to db relation."""
|
||||||
harness.update_relation_data(
|
harness.update_relation_data(
|
||||||
rel_id,
|
rel_id,
|
||||||
'mysql',
|
"mysql",
|
||||||
{
|
{
|
||||||
'databases': json.dumps(['db1']),
|
"databases": json.dumps(["db1"]),
|
||||||
'data': json.dumps({
|
"data": json.dumps(
|
||||||
'credentials': {
|
{
|
||||||
'username': 'foo',
|
"credentials": {
|
||||||
'password': 'hardpassword',
|
"username": "foo",
|
||||||
'address': '10.0.0.10'}})})
|
"password": "hardpassword",
|
||||||
|
"address": "10.0.0.10",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_api_relations(harness):
|
def add_api_relations(harness: Harness) -> None:
|
||||||
add_db_relation_credentials(
|
"""Add standard relation to api charm."""
|
||||||
harness,
|
add_db_relation_credentials(harness, add_base_db_relation(harness))
|
||||||
add_base_db_relation(harness))
|
add_amqp_relation_credentials(harness, add_base_amqp_relation(harness))
|
||||||
add_amqp_relation_credentials(
|
add_identity_service_relation_response(
|
||||||
harness,
|
harness, add_base_identity_service_relation(harness)
|
||||||
add_base_amqp_relation(harness))
|
)
|
||||||
add_identity_service_relation_response(
|
|
||||||
harness,
|
|
||||||
add_base_identity_service_relation(harness))
|
|
||||||
|
|
||||||
|
|
||||||
def get_harness(charm_class, charm_metadata=None, container_calls=None,
|
def get_harness(
|
||||||
charm_config=None):
|
charm_class: ops.charm.CharmBase,
|
||||||
|
charm_metadata: dict = None,
|
||||||
|
container_calls: dict = None,
|
||||||
|
charm_config: dict = None,
|
||||||
|
) -> Harness:
|
||||||
|
"""Return a testing harness."""
|
||||||
|
|
||||||
class _OSTestingPebbleClient(_TestingPebbleClient):
|
class _OSTestingPebbleClient(_TestingPebbleClient):
|
||||||
|
|
||||||
def push(
|
def push(
|
||||||
self, path, source, *,
|
self,
|
||||||
encoding='utf-8', make_dirs=False, permissions=None,
|
path: str,
|
||||||
user_id=None, user=None, group_id=None, group=None):
|
source: typing.Union[bytes, str, typing.BinaryIO, typing.TextIO],
|
||||||
container_calls['push'][path] = {
|
*,
|
||||||
'source': source,
|
encoding: str = "utf-8",
|
||||||
'permissions': permissions,
|
make_dirs: bool = False,
|
||||||
'user': user,
|
permissions: int = None,
|
||||||
'group': group}
|
user_id: int = None,
|
||||||
|
user: str = None,
|
||||||
|
group_id: int = None,
|
||||||
|
group: str = None,
|
||||||
|
) -> None:
|
||||||
|
"""Capture push events and store in container_calls."""
|
||||||
|
container_calls["push"][path] = {
|
||||||
|
"source": source,
|
||||||
|
"permissions": permissions,
|
||||||
|
"user": user,
|
||||||
|
"group": group,
|
||||||
|
}
|
||||||
|
|
||||||
def pull(self, path, *, encoding='utf-8'):
|
def pull(self, path: str, *, encoding: str = "utf-8") -> None:
|
||||||
container_calls['pull'].append(path)
|
"""Capture pull events and store in container_calls."""
|
||||||
|
container_calls["pull"].append(path)
|
||||||
reader = io.StringIO("0")
|
reader = io.StringIO("0")
|
||||||
return reader
|
return reader
|
||||||
|
|
||||||
def remove_path(self, path, *, recursive=False):
|
def remove_path(self, path: str, *, recursive: bool = False) -> None:
|
||||||
container_calls['remove_path'].append(path)
|
"""Capture remove events and store in container_calls."""
|
||||||
|
container_calls["remove_path"].append(path)
|
||||||
|
|
||||||
class _OSTestingModelBackend(_TestingModelBackend):
|
class _OSTestingModelBackend(_TestingModelBackend):
|
||||||
|
def get_pebble(self, socket_path: str) -> _OSTestingPebbleClient:
|
||||||
def get_pebble(self, socket_path: str):
|
"""Get the testing pebble client."""
|
||||||
client = self._pebble_clients.get(socket_path, None)
|
client = self._pebble_clients.get(socket_path, None)
|
||||||
if client is None:
|
if client is None:
|
||||||
client = _OSTestingPebbleClient(self)
|
client = _OSTestingPebbleClient(self)
|
||||||
self._pebble_clients[socket_path] = client
|
self._pebble_clients[socket_path] = client
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def network_get(self, endpoint_name, relation_id=None):
|
def network_get(
|
||||||
|
self, endpoint_name: str, relation_id: str = None
|
||||||
|
) -> dict:
|
||||||
|
"""Return a fake set of network data."""
|
||||||
network_data = {
|
network_data = {
|
||||||
'bind-addresses': [{
|
"bind-addresses": [
|
||||||
'interface-name': 'eth0',
|
{
|
||||||
'addresses': [{
|
"interface-name": "eth0",
|
||||||
'cidr': '10.0.0.0/24',
|
"addresses": [
|
||||||
'value': '10.0.0.10'}]}],
|
{"cidr": "10.0.0.0/24", "value": "10.0.0.10"}
|
||||||
'ingress-addresses': ['10.0.0.10'],
|
],
|
||||||
'egress-subnets': ['10.0.0.0/24']}
|
}
|
||||||
|
],
|
||||||
|
"ingress-addresses": ["10.0.0.10"],
|
||||||
|
"egress-subnets": ["10.0.0.0/24"],
|
||||||
|
}
|
||||||
return network_data
|
return network_data
|
||||||
|
|
||||||
filename = inspect.getfile(charm_class)
|
filename = inspect.getfile(charm_class)
|
||||||
charm_dir = pathlib.Path(filename).parents[1]
|
charm_dir = pathlib.Path(filename).parents[1]
|
||||||
|
|
||||||
if not charm_metadata:
|
if not charm_metadata:
|
||||||
metadata_file = f'{charm_dir}/metadata.yaml'
|
metadata_file = f"{charm_dir}/metadata.yaml"
|
||||||
if os.path.isfile(metadata_file):
|
if os.path.isfile(metadata_file):
|
||||||
with open(metadata_file) as f:
|
with open(metadata_file) as f:
|
||||||
charm_metadata = f.read()
|
charm_metadata = f.read()
|
||||||
if not charm_config:
|
if not charm_config:
|
||||||
config_file = f'{charm_dir}/config.yaml'
|
config_file = f"{charm_dir}/config.yaml"
|
||||||
if os.path.isfile(config_file):
|
if os.path.isfile(config_file):
|
||||||
with open(config_file) as f:
|
with open(config_file) as f:
|
||||||
charm_config = yaml.safe_load(f.read())['options']
|
charm_config = yaml.safe_load(f.read())["options"]
|
||||||
|
|
||||||
harness = Harness(
|
harness = Harness(
|
||||||
charm_class,
|
charm_class,
|
||||||
meta=charm_metadata,
|
meta=charm_metadata,
|
||||||
)
|
)
|
||||||
harness._backend = _OSTestingModelBackend(
|
harness._backend = _OSTestingModelBackend(
|
||||||
harness._unit_name, harness._meta)
|
harness._unit_name, harness._meta
|
||||||
harness._model = model.Model(
|
)
|
||||||
harness._meta,
|
harness._model = model.Model(harness._meta, harness._backend)
|
||||||
harness._backend)
|
|
||||||
harness._framework = framework.Framework(
|
harness._framework = framework.Framework(
|
||||||
":memory:",
|
":memory:", harness._charm_dir, harness._meta, harness._model
|
||||||
harness._charm_dir,
|
)
|
||||||
harness._meta,
|
|
||||||
harness._model)
|
|
||||||
if charm_config:
|
if charm_config:
|
||||||
harness.update_config(charm_config)
|
harness.update_config(charm_config)
|
||||||
return harness
|
return harness
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# This file is managed centrally. If you find the need to modify this as a
|
flake8-annotations
|
||||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
flake8-docstrings
|
||||||
# requirements management in charms via bot-control. Thank you.
|
|
||||||
charm-tools>=2.4.4
|
charm-tools>=2.4.4
|
||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
mock>=1.2
|
mock>=1.2
|
||||||
flake8>=2.2.4,<=2.4.1
|
flake8
|
||||||
pyflakes==2.1.1
|
pyflakes
|
||||||
stestr>=2.2.0
|
stestr>=2.2.0
|
||||||
requests>=2.18.4
|
requests>=2.18.4
|
||||||
psutil
|
psutil
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Operator charm (with zaza): tox.ini
|
# Operator charm helper: tox.ini
|
||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
envlist = pep8,fetch,py3
|
envlist = pep8,fetch,py3
|
||||||
@ -42,16 +42,6 @@ deps =
|
|||||||
commands =
|
commands =
|
||||||
./fetch-libs.sh
|
./fetch-libs.sh
|
||||||
|
|
||||||
[testenv:py35]
|
|
||||||
basepython = python3.5
|
|
||||||
# python3.5 is irrelevant on a focal+ charm.
|
|
||||||
commands = /bin/true
|
|
||||||
|
|
||||||
[testenv:py36]
|
|
||||||
basepython = python3.6
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
|
|
||||||
[testenv:py37]
|
[testenv:py37]
|
||||||
basepython = python3.7
|
basepython = python3.7
|
||||||
deps = -r{toxinidir}/requirements.txt
|
deps = -r{toxinidir}/requirements.txt
|
||||||
@ -73,23 +63,6 @@ deps = -r{toxinidir}/requirements.txt
|
|||||||
-r{toxinidir}/test-requirements.txt
|
-r{toxinidir}/test-requirements.txt
|
||||||
commands = flake8 {posargs} src unit_tests tests advanced_sunbeam_openstack
|
commands = flake8 {posargs} src unit_tests tests advanced_sunbeam_openstack
|
||||||
|
|
||||||
[testenv:cover]
|
|
||||||
# Technique based heavily upon
|
|
||||||
# https://github.com/openstack/nova/blob/master/tox.ini
|
|
||||||
basepython = python3
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
setenv =
|
|
||||||
{[testenv]setenv}
|
|
||||||
PYTHON=coverage run
|
|
||||||
commands =
|
|
||||||
coverage erase
|
|
||||||
stestr run --slowest {posargs}
|
|
||||||
coverage combine
|
|
||||||
coverage html -d cover
|
|
||||||
coverage xml -o cover/coverage.xml
|
|
||||||
coverage report
|
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
branch = True
|
branch = True
|
||||||
concurrency = multiprocessing
|
concurrency = multiprocessing
|
||||||
@ -105,37 +78,6 @@ omit =
|
|||||||
basepython = python3
|
basepython = python3
|
||||||
commands = {posargs}
|
commands = {posargs}
|
||||||
|
|
||||||
[testenv:build]
|
|
||||||
basepython = python3
|
|
||||||
deps = -r{toxinidir}/build-requirements.txt
|
|
||||||
commands =
|
|
||||||
charmcraft build
|
|
||||||
|
|
||||||
[testenv:func-noop]
|
|
||||||
basepython = python3
|
|
||||||
commands =
|
|
||||||
functest-run-suite --help
|
|
||||||
|
|
||||||
[testenv:func]
|
|
||||||
basepython = python3
|
|
||||||
commands =
|
|
||||||
functest-run-suite --keep-model
|
|
||||||
|
|
||||||
[testenv:func-smoke]
|
|
||||||
basepython = python3
|
|
||||||
commands =
|
|
||||||
functest-run-suite --keep-model --smoke
|
|
||||||
|
|
||||||
[testenv:func-dev]
|
|
||||||
basepython = python3
|
|
||||||
commands =
|
|
||||||
functest-run-suite --keep-model --dev
|
|
||||||
|
|
||||||
[testenv:func-target]
|
|
||||||
basepython = python3
|
|
||||||
commands =
|
|
||||||
functest-run-suite --keep-model --bundle {posargs}
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
# Ignore E902 because the unit_tests directory is missing in the built charm.
|
# Ignore E902 because the unit_tests directory is missing in the built charm.
|
||||||
ignore = E402,E226,E902
|
ignore = E402,E226,E902,ANN101,ANN003
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2021 Canonical Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unit tests for aso."""
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Copyright 2020 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.
|
||||||
@ -14,21 +14,25 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Test charms for unit tests."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append('lib') # noqa
|
from typing import TYPE_CHECKING
|
||||||
sys.path.append('src') # noqa
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import ops.framework
|
||||||
|
|
||||||
|
sys.path.append("lib") # noqa
|
||||||
|
sys.path.append("src") # noqa
|
||||||
|
|
||||||
import advanced_sunbeam_openstack.charm as sunbeam_charm
|
import advanced_sunbeam_openstack.charm as sunbeam_charm
|
||||||
|
|
||||||
CHARM_CONFIG = {
|
CHARM_CONFIG = {"region": "RegionOne", "debug": "true"}
|
||||||
'region': 'RegionOne',
|
|
||||||
'debug': 'true'}
|
|
||||||
|
|
||||||
CHARM_METADATA = '''
|
CHARM_METADATA = """
|
||||||
name: my-service
|
name: my-service
|
||||||
version: 3
|
version: 3
|
||||||
bases:
|
bases:
|
||||||
@ -57,9 +61,9 @@ storage:
|
|||||||
resources:
|
resources:
|
||||||
mysvc-image:
|
mysvc-image:
|
||||||
type: oci-image
|
type: oci-image
|
||||||
'''
|
"""
|
||||||
|
|
||||||
API_CHARM_METADATA = '''
|
API_CHARM_METADATA = """
|
||||||
name: my-service
|
name: my-service
|
||||||
version: 3
|
version: 3
|
||||||
bases:
|
bases:
|
||||||
@ -103,48 +107,60 @@ storage:
|
|||||||
resources:
|
resources:
|
||||||
mysvc-image:
|
mysvc-image:
|
||||||
type: oci-image
|
type: oci-image
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MyCharm(sunbeam_charm.OSBaseOperatorCharm):
|
class MyCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||||
|
"""Test charm for testing OSBaseOperatorCharm."""
|
||||||
|
|
||||||
openstack_release = 'diablo'
|
openstack_release = "diablo"
|
||||||
service_name = 'my-service'
|
service_name = "my-service"
|
||||||
|
|
||||||
def __init__(self, framework):
|
def __init__(self, framework: "ops.framework.Framework") -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.seen_events = []
|
self.seen_events = []
|
||||||
self.render_calls = []
|
self.render_calls = []
|
||||||
self._template_dir = self._setup_templates()
|
self._template_dir = self._setup_templates()
|
||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
|
|
||||||
def _log_event(self, event):
|
def _log_event(self, event: "ops.framework.EventBase") -> None:
|
||||||
|
"""Log events."""
|
||||||
self.seen_events.append(type(event).__name__)
|
self.seen_events.append(type(event).__name__)
|
||||||
|
|
||||||
def _on_service_pebble_ready(self, event):
|
def _on_service_pebble_ready(
|
||||||
|
self, event: "ops.framework.EventBase"
|
||||||
|
) -> None:
|
||||||
|
"""Log pebble ready event."""
|
||||||
|
self._log_event(event)
|
||||||
super()._on_service_pebble_ready(event)
|
super()._on_service_pebble_ready(event)
|
||||||
self._log_event(event)
|
|
||||||
|
|
||||||
def _on_config_changed(self, event):
|
def _on_config_changed(self, event: "ops.framework.EventBase") -> None:
|
||||||
|
"""Log config changed event."""
|
||||||
self._log_event(event)
|
self._log_event(event)
|
||||||
|
super()._on_config_changed(event)
|
||||||
|
|
||||||
def configure_charm(self, event):
|
def configure_charm(self, event: "ops.framework.EventBase") -> None:
|
||||||
|
"""Log configure_charm call."""
|
||||||
|
self._log_event(event)
|
||||||
super().configure_charm(event)
|
super().configure_charm(event)
|
||||||
self._log_event(event)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_ingress_port(self):
|
def public_ingress_port(self) -> int:
|
||||||
|
"""Charms default port."""
|
||||||
return 789
|
return 789
|
||||||
|
|
||||||
def _setup_templates(self):
|
def _setup_templates(self) -> str:
|
||||||
|
"""Run temp templates dir setup."""
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
_template_dir = f'{tmpdir}/templates'
|
_template_dir = f"{tmpdir}/templates"
|
||||||
os.mkdir(_template_dir)
|
os.mkdir(_template_dir)
|
||||||
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
|
with open(f"{_template_dir}/my-service.conf.j2", "w") as f:
|
||||||
f.write("")
|
f.write("")
|
||||||
return _template_dir
|
return _template_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def template_dir(self):
|
def template_dir(self) -> str:
|
||||||
|
"""Temp templates dir."""
|
||||||
return self._template_dir
|
return self._template_dir
|
||||||
|
|
||||||
|
|
||||||
@ -160,41 +176,53 @@ TEMPLATE_CONTENTS = """
|
|||||||
|
|
||||||
|
|
||||||
class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||||
openstack_release = 'diablo'
|
"""Test charm for testing OSBaseOperatorAPICharm."""
|
||||||
service_name = 'my-service'
|
|
||||||
wsgi_admin_script = '/bin/wsgi_admin'
|
|
||||||
wsgi_public_script = '/bin/wsgi_public'
|
|
||||||
|
|
||||||
def __init__(self, framework):
|
openstack_release = "diablo"
|
||||||
|
service_name = "my-service"
|
||||||
|
wsgi_admin_script = "/bin/wsgi_admin"
|
||||||
|
wsgi_public_script = "/bin/wsgi_public"
|
||||||
|
|
||||||
|
def __init__(self, framework: "ops.framework.Framework") -> None:
|
||||||
|
"""Run constructor."""
|
||||||
self.seen_events = []
|
self.seen_events = []
|
||||||
self.render_calls = []
|
self.render_calls = []
|
||||||
self._template_dir = self._setup_templates()
|
self._template_dir = self._setup_templates()
|
||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
|
|
||||||
def _setup_templates(self):
|
def _setup_templates(self) -> str:
|
||||||
|
"""Run temp templates dir setup."""
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
_template_dir = f'{tmpdir}/templates'
|
_template_dir = f"{tmpdir}/templates"
|
||||||
os.mkdir(_template_dir)
|
os.mkdir(_template_dir)
|
||||||
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
|
with open(f"{_template_dir}/my-service.conf.j2", "w") as f:
|
||||||
f.write(TEMPLATE_CONTENTS)
|
f.write(TEMPLATE_CONTENTS)
|
||||||
with open(f'{_template_dir}/wsgi-my-service.conf.j2', 'w') as f:
|
with open(f"{_template_dir}/wsgi-my-service.conf.j2", "w") as f:
|
||||||
f.write(TEMPLATE_CONTENTS)
|
f.write(TEMPLATE_CONTENTS)
|
||||||
return _template_dir
|
return _template_dir
|
||||||
|
|
||||||
def _log_event(self, event):
|
def _log_event(self, event: "ops.framework.EventBase") -> None:
|
||||||
|
"""Log events."""
|
||||||
self.seen_events.append(type(event).__name__)
|
self.seen_events.append(type(event).__name__)
|
||||||
|
|
||||||
def _on_service_pebble_ready(self, event):
|
def _on_service_pebble_ready(
|
||||||
|
self, event: "ops.framework.EventBase"
|
||||||
|
) -> None:
|
||||||
|
"""Log pebble ready event."""
|
||||||
|
self._log_event(event)
|
||||||
super()._on_service_pebble_ready(event)
|
super()._on_service_pebble_ready(event)
|
||||||
self._log_event(event)
|
|
||||||
|
|
||||||
def _on_config_changed(self, event):
|
def _on_config_changed(self, event: "ops.framework.EventBase") -> None:
|
||||||
|
"""Log config changed event."""
|
||||||
self._log_event(event)
|
self._log_event(event)
|
||||||
|
super()._on_config_changed(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_public_ingress_port(self):
|
def default_public_ingress_port(self) -> int:
|
||||||
|
"""Charms default port."""
|
||||||
return 789
|
return 789
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def template_dir(self):
|
def template_dir(self) -> str:
|
||||||
|
"""Templates dir."""
|
||||||
return self._template_dir
|
return self._template_dir
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
# Copyright 2021 Canonical Ltd.
|
||||||
|
|
||||||
# Copyright 2020 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.
|
||||||
@ -14,9 +12,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Test aso."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from mock import patch
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append('lib') # noqa
|
sys.path.append('lib') # noqa
|
||||||
@ -28,11 +26,13 @@ from . import test_charms
|
|||||||
|
|
||||||
|
|
||||||
class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
|
class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
|
||||||
|
"""Test for the OSBaseOperatorCharm class."""
|
||||||
|
|
||||||
PATCHES = [
|
PATCHES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self) -> None:
|
||||||
|
"""Charm test class setup."""
|
||||||
self.container_calls = {
|
self.container_calls = {
|
||||||
'push': {},
|
'push': {},
|
||||||
'pull': [],
|
'pull': [],
|
||||||
@ -46,43 +46,45 @@ class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
|
|||||||
self.harness.begin()
|
self.harness.begin()
|
||||||
self.addCleanup(self.harness.cleanup)
|
self.addCleanup(self.harness.cleanup)
|
||||||
|
|
||||||
def set_pebble_ready(self):
|
def set_pebble_ready(self) -> None:
|
||||||
|
"""Set pebble ready event."""
|
||||||
container = self.harness.model.unit.get_container("my-service")
|
container = self.harness.model.unit.get_container("my-service")
|
||||||
# Emit the PebbleReadyEvent
|
# Emit the PebbleReadyEvent
|
||||||
self.harness.charm.on.my_service_pebble_ready.emit(container)
|
self.harness.charm.on.my_service_pebble_ready.emit(container)
|
||||||
|
|
||||||
def test_pebble_ready_handler(self):
|
def test_pebble_ready_handler(self) -> None:
|
||||||
|
"""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):
|
def test_write_config(self) -> None:
|
||||||
|
"""Test writing config when charm is ready."""
|
||||||
self.set_pebble_ready()
|
self.set_pebble_ready()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.container_calls['push'],
|
self.container_calls['push'],
|
||||||
{})
|
{})
|
||||||
|
|
||||||
def test_handler_prefix(self):
|
def test_container_names(self) -> None:
|
||||||
self.assertEqual(
|
"""Test container name list is correct."""
|
||||||
self.harness.charm.handler_prefix,
|
|
||||||
'my_service')
|
|
||||||
|
|
||||||
def test_container_names(self):
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.harness.charm.container_names,
|
self.harness.charm.container_names,
|
||||||
['my-service'])
|
['my-service'])
|
||||||
|
|
||||||
def test_relation_handlers_ready(self):
|
def test_relation_handlers_ready(self) -> None:
|
||||||
|
"""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):
|
||||||
|
"""Test for the OSBaseOperatorAPICharm class."""
|
||||||
|
|
||||||
PATCHES = [
|
PATCHES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self) -> None:
|
||||||
|
"""Charm test class setup."""
|
||||||
self.container_calls = {
|
self.container_calls = {
|
||||||
'push': {},
|
'push': {},
|
||||||
'pull': [],
|
'pull': [],
|
||||||
@ -98,10 +100,12 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
|
|||||||
self.harness.update_config(test_charms.CHARM_CONFIG)
|
self.harness.update_config(test_charms.CHARM_CONFIG)
|
||||||
self.harness.begin()
|
self.harness.begin()
|
||||||
|
|
||||||
def set_pebble_ready(self):
|
def set_pebble_ready(self) -> None:
|
||||||
|
"""Set pebble ready event."""
|
||||||
self.harness.container_pebble_ready('my-service')
|
self.harness.container_pebble_ready('my-service')
|
||||||
|
|
||||||
def test_write_config(self):
|
def test_write_config(self) -> None:
|
||||||
|
"""Test when charm is ready configs are written correctly."""
|
||||||
self.harness.set_leader()
|
self.harness.set_leader()
|
||||||
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(
|
||||||
@ -135,8 +139,8 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
|
|||||||
'source': expect_string,
|
'source': expect_string,
|
||||||
'user': 'root'})
|
'user': 'root'})
|
||||||
|
|
||||||
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
|
def test__on_database_changed(self) -> None:
|
||||||
def test__on_database_changed(self, _renderer):
|
"""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,
|
rel_id,
|
||||||
@ -149,10 +153,10 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
|
|||||||
db_rel_id,
|
db_rel_id,
|
||||||
'my-service')
|
'my-service')
|
||||||
requested_db = json.loads(rel_data['databases'])[0]
|
requested_db = json.loads(rel_data['databases'])[0]
|
||||||
# self.assertRegex(requested_db, r'^db_.*my_service$')
|
|
||||||
self.assertEqual(requested_db, 'my_service')
|
self.assertEqual(requested_db, 'my_service')
|
||||||
|
|
||||||
def test_contexts(self):
|
def test_contexts(self) -> None:
|
||||||
|
"""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,
|
rel_id,
|
||||||
@ -172,7 +176,8 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
|
|||||||
contexts.options.debug,
|
contexts.options.debug,
|
||||||
'true')
|
'true')
|
||||||
|
|
||||||
def test_peer_leader_db(self):
|
def test_peer_leader_db(self) -> None:
|
||||||
|
"""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,
|
rel_id,
|
||||||
@ -195,7 +200,8 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
|
|||||||
self.harness.charm.leader_get('ginger'),
|
self.harness.charm.leader_get('ginger'),
|
||||||
'biscuit')
|
'biscuit')
|
||||||
|
|
||||||
def test_peer_leader_ready(self):
|
def test_peer_leader_ready(self) -> None:
|
||||||
|
"""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,
|
rel_id,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user