Remove RelationAdapters etc
Remove RelationAdapters and add contexts to relation handlers. Other thing of note is that default handlers are registered based on a charms metadata.
This commit is contained in:
parent
e2f63e897d
commit
15a9842bd2
@ -1,178 +0,0 @@
|
||||
import logging
|
||||
import ops_openstack.adapters
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigAdapter():
|
||||
|
||||
def __init__(self, charm, namespace):
|
||||
self.charm = charm
|
||||
self.namespace = namespace
|
||||
for k, v in self.context().items():
|
||||
k = k.replace('-', '_')
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return True
|
||||
|
||||
|
||||
class CharmConfigAdapter(ConfigAdapter):
|
||||
|
||||
def context(self):
|
||||
return self.charm.config
|
||||
|
||||
|
||||
class WSGIWorkerConfigAdapter(ConfigAdapter):
|
||||
|
||||
def context(self):
|
||||
return {
|
||||
'name': self.charm.service_name,
|
||||
'wsgi_admin_script': self.charm.wsgi_admin_script,
|
||||
'wsgi_public_script': self.charm.wsgi_public_script}
|
||||
|
||||
|
||||
class DBAdapter(ops_openstack.adapters.OpenStackOperRelationAdapter):
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
return self.relation.databases()[0]
|
||||
|
||||
@property
|
||||
def database_host(self):
|
||||
return self.relation.credentials().get('address')
|
||||
|
||||
@property
|
||||
def database_password(self):
|
||||
return self.relation.credentials().get('password')
|
||||
|
||||
@property
|
||||
def database_user(self):
|
||||
return self.relation.credentials().get('username')
|
||||
|
||||
@property
|
||||
def database_type(self):
|
||||
return 'mysql+pymysql'
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
try:
|
||||
creds = self.relation.credentials()
|
||||
except AttributeError:
|
||||
return False
|
||||
return bool(creds)
|
||||
|
||||
|
||||
class AMQPAdapter(ops_openstack.adapters.OpenStackOperRelationAdapter):
|
||||
|
||||
DEFAULT_PORT = "5672"
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""Return the AMQP port
|
||||
|
||||
:returns: AMQP port number
|
||||
:rtype: string
|
||||
"""
|
||||
return self.relation.ssl_port or self.DEFAULT_PORT
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
"""
|
||||
Comma separated list of hosts that should be used
|
||||
to access RabbitMQ.
|
||||
"""
|
||||
hosts = self.relation.hostnames
|
||||
if len(hosts) > 1:
|
||||
return ','.join(hosts)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def transport_url(self) -> str:
|
||||
"""
|
||||
oslo.messaging formatted transport URL
|
||||
|
||||
:returns: oslo.messaging formatted transport URL
|
||||
:rtype: string
|
||||
"""
|
||||
hosts = self.relation.hostnames
|
||||
transport_url_hosts = ','.join([
|
||||
"{}:{}@{}:{}".format(self.username,
|
||||
self.password,
|
||||
host_, # TODO deal with IPv6
|
||||
self.port)
|
||||
for host_ in hosts
|
||||
])
|
||||
return "rabbit://{}/{}".format(transport_url_hosts, self.vhost)
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
try:
|
||||
creds = self.transport_url
|
||||
except AttributeError:
|
||||
return False
|
||||
return bool(creds)
|
||||
|
||||
|
||||
class OPSRelationAdapters():
|
||||
|
||||
def __init__(self, charm):
|
||||
self.charm = charm
|
||||
self.namespaces = []
|
||||
|
||||
def _get_adapter(self, relation_name):
|
||||
# Matching relation first
|
||||
# Then interface name
|
||||
if self.relation_map.get(relation_name):
|
||||
return self.relation_map.get(relation_name)
|
||||
interface_name = self.charm.meta.relations[
|
||||
relation_name].interface_name
|
||||
if self.interface_map.get(interface_name):
|
||||
return self.interface_map.get(interface_name)
|
||||
|
||||
def add_relation_adapter(self, interface, relation_name):
|
||||
adapter = self._get_adapter(relation_name)
|
||||
if adapter:
|
||||
adapter_ns = relation_name.replace("-", "_")
|
||||
self.namespaces.append(adapter_ns)
|
||||
setattr(self, adapter_ns, adapter(interface))
|
||||
else:
|
||||
logging.debug(f"No adapter found for {relation_name}")
|
||||
|
||||
def add_config_adapters(self, config_adapters):
|
||||
for config_adapter in config_adapters:
|
||||
self.add_config_adapter(
|
||||
config_adapter,
|
||||
config_adapter.namespace)
|
||||
|
||||
def add_config_adapter(self, config_adapter, namespace):
|
||||
self.namespaces.append(namespace)
|
||||
setattr(self, namespace, config_adapter)
|
||||
|
||||
@property
|
||||
def interface_map(self):
|
||||
return {}
|
||||
|
||||
@property
|
||||
def relation_map(self):
|
||||
return {}
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate over the relations presented to the charm.
|
||||
"""
|
||||
for namespace in self.namespaces:
|
||||
yield namespace, getattr(self, namespace)
|
||||
|
||||
|
||||
class APICharmAdapters(OPSRelationAdapters):
|
||||
"""Collection of relation adapters."""
|
||||
|
||||
@property
|
||||
def interface_map(self):
|
||||
_map = super().interface_map
|
||||
_map.update({
|
||||
'mysql_datastore': DBAdapter})
|
||||
return _map
|
264
ops-sunbeam/advanced_sunbeam_openstack/charm.py
Normal file
264
ops-sunbeam/advanced_sunbeam_openstack/charm.py
Normal file
@ -0,0 +1,264 @@
|
||||
# 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.
|
||||
|
||||
"""Base classes for defining a charm using the Operator framework.
|
||||
|
||||
This library provided OSBaseOperatorCharm and OSBaseOperatorAPICharm. The
|
||||
charm classes use advanced_sunbeam_openstack.relation_handlers.RelationHandler
|
||||
objects to interact with relations. These objects also provide contexts which
|
||||
can be used when defining templates.
|
||||
|
||||
In addition to the Relation handlers the charm class can also use
|
||||
advanced_sunbeam_openstack.config_contexts.ConfigContext objects which
|
||||
can be used when rendering templates, these are not specific to a relation.
|
||||
|
||||
The charm class interacts with the containers it is managing via
|
||||
advanced_sunbeam_openstack.container_handlers.PebbleHandler. The
|
||||
PebbleHandler defines the pebble layers, manages pushing
|
||||
configuration to the containers and managing the service running
|
||||
in the container.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import ops.charm
|
||||
import ops.framework
|
||||
import ops.model
|
||||
|
||||
import advanced_sunbeam_openstack.config_contexts as sunbeam_config_contexts
|
||||
import advanced_sunbeam_openstack.container_handlers as sunbeam_chandlers
|
||||
import advanced_sunbeam_openstack.core as sunbeam_core
|
||||
import advanced_sunbeam_openstack.relation_handlers as sunbeam_rhandlers
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OSBaseOperatorCharm(ops.charm.CharmBase):
|
||||
"""Base charms for OpenStack operators."""
|
||||
|
||||
_state = ops.framework.StoredState()
|
||||
|
||||
def __init__(self, framework):
|
||||
super().__init__(framework)
|
||||
self._state.set_default(bootstrapped=False)
|
||||
self.relation_handlers = self.get_relation_handlers()
|
||||
self.pebble_handlers = self.get_pebble_handlers()
|
||||
self.framework.observe(self.on.config_changed,
|
||||
self._on_config_changed)
|
||||
|
||||
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||
"""Relation handlers for the operator."""
|
||||
return []
|
||||
|
||||
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
|
||||
"""Pebble handlers for the operator."""
|
||||
return [
|
||||
sunbeam_chandlers.PebbleHandler(
|
||||
self,
|
||||
self.service_name,
|
||||
self.service_name,
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.configure_charm)]
|
||||
|
||||
def configure_charm(self, event) -> None:
|
||||
"""Catchall handler to cconfigure charm services."""
|
||||
if not self.relation_handlers_ready():
|
||||
logging.debug("Aborting charm relations not ready")
|
||||
return
|
||||
|
||||
for ph in self.pebble_handlers:
|
||||
if ph.pebble_ready:
|
||||
ph.init_service(self.contexts())
|
||||
|
||||
for ph in self.pebble_handlers:
|
||||
if not ph.service_ready:
|
||||
logging.debug("Aborting container service not ready")
|
||||
return
|
||||
|
||||
if not self.bootstrapped():
|
||||
self._do_bootstrap()
|
||||
|
||||
self.unit.status = ops.model.ActiveStatus()
|
||||
self._state.bootstrapped = True
|
||||
|
||||
@property
|
||||
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
||||
"""Container configuration files for the operator."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def config_contexts(self) -> List[
|
||||
sunbeam_config_contexts.CharmConfigContext]:
|
||||
"""Configuration adapters for the operator."""
|
||||
return [
|
||||
sunbeam_config_contexts.CharmConfigContext(self, 'options')]
|
||||
|
||||
@property
|
||||
def handler_prefix(self) -> str:
|
||||
"""Prefix for handlers??"""
|
||||
return self.service_name.replace('-', '_')
|
||||
|
||||
@property
|
||||
def container_names(self):
|
||||
"""Containers that form part of this service."""
|
||||
return [self.service_name]
|
||||
|
||||
@property
|
||||
def template_dir(self) -> str:
|
||||
"""Directory containing Jinja2 templates."""
|
||||
return 'src/templates'
|
||||
|
||||
def _on_config_changed(self, event):
|
||||
self.configure_charm(None)
|
||||
|
||||
def containers_ready(self) -> bool:
|
||||
"""Determine whether all containers are ready for configuration."""
|
||||
for ph in self.pebble_handlers:
|
||||
if not ph.service_ready:
|
||||
logger.info(f"Container incomplete: {ph.container_name}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def relation_handlers_ready(self) -> bool:
|
||||
"""Determine whether all relations are ready for use."""
|
||||
for handler in self.relation_handlers:
|
||||
if not handler.ready:
|
||||
logger.info(f"Relation {handler.relation_name} incomplete")
|
||||
return False
|
||||
return True
|
||||
|
||||
def contexts(self) -> sunbeam_core.OPSCharmContexts:
|
||||
"""Construct context for rendering templates."""
|
||||
ra = sunbeam_core.OPSCharmContexts(self)
|
||||
for handler in self.relation_handlers:
|
||||
if handler.relation_name not in self.meta.relations.keys():
|
||||
logger.info(
|
||||
f"Dropping handler for relation {handler.relation_name}, "
|
||||
"relation not present in charm metadata")
|
||||
continue
|
||||
if handler.ready:
|
||||
ra.add_relation_handler(handler)
|
||||
ra.add_config_contexts(self.config_contexts)
|
||||
return ra
|
||||
|
||||
def _do_bootstrap(self) -> None:
|
||||
"""Bootstrap the service ready for operation.
|
||||
|
||||
This method should be overridden as part of a concrete
|
||||
charm implementation
|
||||
"""
|
||||
pass
|
||||
|
||||
def bootstrapped(self) -> bool:
|
||||
"""Determine whether the service has been boostrapped."""
|
||||
return self._state.bootstrapped
|
||||
|
||||
|
||||
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
||||
"""Base class for OpenStack API operators"""
|
||||
|
||||
def __init__(self, framework):
|
||||
super().__init__(framework)
|
||||
self._state.set_default(db_ready=False)
|
||||
|
||||
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
|
||||
"""Pebble handlers for the service"""
|
||||
return [
|
||||
sunbeam_chandlers.WSGIPebbleHandler(
|
||||
self,
|
||||
self.service_name,
|
||||
self.service_name,
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.configure_charm,
|
||||
f'wsgi-{self.service_name}')]
|
||||
|
||||
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||
"""Relation handlers for the service."""
|
||||
handlers = []
|
||||
if 'amqp' in self.meta.relations.keys():
|
||||
self.amqp = sunbeam_rhandlers.AMQPHandler(
|
||||
self,
|
||||
'amqp',
|
||||
self.configure_charm,
|
||||
self.service_name,
|
||||
self.service_name)
|
||||
handlers.append(self.amqp)
|
||||
if f'{self.service_name}-db' in self.meta.relations.keys():
|
||||
self.db = sunbeam_rhandlers.DBHandler(
|
||||
self,
|
||||
f'{self.service_name}-db',
|
||||
self.configure_charm)
|
||||
handlers.append(self.db)
|
||||
if 'ingress' in self.meta.relations.keys():
|
||||
self.ingress = sunbeam_rhandlers.IngressHandler(
|
||||
self,
|
||||
'ingress',
|
||||
self.service_name,
|
||||
self.default_public_ingress_port,
|
||||
self.configure_charm)
|
||||
handlers.append(self.ingress)
|
||||
return handlers
|
||||
|
||||
@property
|
||||
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
||||
"""Container configuration files for the service."""
|
||||
_cconfigs = super().container_configs
|
||||
_cconfigs.extend([
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.wsgi_container_name],
|
||||
self.service_conf,
|
||||
self.service_user,
|
||||
self.service_group)])
|
||||
return _cconfigs
|
||||
|
||||
@property
|
||||
def service_user(self) -> str:
|
||||
"""Service user file and directory ownership."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def service_group(self) -> str:
|
||||
"""Service group file and directory ownership."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def service_conf(self) -> str:
|
||||
"""Service default configuration file."""
|
||||
return f'/etc/{self.service_name}/{self.service_name}.conf'
|
||||
|
||||
@property
|
||||
def config_contexts(self) -> List[sunbeam_config_contexts.ConfigContext]:
|
||||
"""Generate list of configuration adapters for the charm."""
|
||||
_cadapters = super().config_contexts
|
||||
_cadapters.extend([
|
||||
sunbeam_config_contexts.WSGIWorkerConfigContext(
|
||||
self,
|
||||
'wsgi_config')])
|
||||
return _cadapters
|
||||
|
||||
@property
|
||||
def wsgi_container_name(self) -> str:
|
||||
"""Name of the WSGI application container."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def default_public_ingress_port(self) -> int:
|
||||
"""Port to use for ingress access to service."""
|
||||
raise NotImplementedError
|
58
ops-sunbeam/advanced_sunbeam_openstack/config_contexts.py
Normal file
58
ops-sunbeam/advanced_sunbeam_openstack/config_contexts.py
Normal file
@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
|
||||
"""Base classes for defining a charm using the Operator framework.
|
||||
|
||||
ConfigContext objects can be used when rendering templates. They idea is to
|
||||
create reusable contexts which translate charm config, deployment state etc.
|
||||
These are not specific to a relation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigContext():
|
||||
|
||||
def __init__(self, charm, namespace):
|
||||
self.charm = charm
|
||||
self.namespace = namespace
|
||||
for k, v in self.context().items():
|
||||
k = k.replace('-', '_')
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return True
|
||||
|
||||
def context(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CharmConfigContext(ConfigContext):
|
||||
"""A context containing all of the charms config options"""
|
||||
|
||||
def context(self) -> dict:
|
||||
return self.charm.config
|
||||
|
||||
|
||||
class WSGIWorkerConfigContext(ConfigContext):
|
||||
|
||||
def context(self) -> dict:
|
||||
"""A context containing WSGI configuration options"""
|
||||
return {
|
||||
'name': self.charm.service_name,
|
||||
'wsgi_admin_script': self.charm.wsgi_admin_script,
|
||||
'wsgi_public_script': self.charm.wsgi_public_script}
|
213
ops-sunbeam/advanced_sunbeam_openstack/container_handlers.py
Normal file
213
ops-sunbeam/advanced_sunbeam_openstack/container_handlers.py
Normal file
@ -0,0 +1,213 @@
|
||||
# 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.
|
||||
|
||||
"""Base classes for defining Pebble handlers
|
||||
|
||||
The PebbleHandler defines the pebble layers, manages pushing
|
||||
configuration to the containers and managing the service running
|
||||
in the container.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import advanced_sunbeam_openstack.core as sunbeam_core
|
||||
import advanced_sunbeam_openstack.templating as sunbeam_templating
|
||||
import advanced_sunbeam_openstack.cprocess as sunbeam_cprocess
|
||||
import ops.charm
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PebbleHandler(ops.charm.Object):
|
||||
"""Base handler for Pebble based containers."""
|
||||
|
||||
_state = ops.framework.StoredState()
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
container_name: str, service_name: str,
|
||||
container_configs: List[sunbeam_core.ContainerConfigFile],
|
||||
template_dir: str, openstack_release: str,
|
||||
callback_f: Callable):
|
||||
super().__init__(charm, None)
|
||||
self._state.set_default(pebble_ready=False)
|
||||
self._state.set_default(config_pushed=False)
|
||||
self._state.set_default(service_ready=False)
|
||||
self.charm = charm
|
||||
self.container_name = container_name
|
||||
self.service_name = service_name
|
||||
self.container_configs = container_configs
|
||||
self.container_configs.extend(self.default_container_configs())
|
||||
self.template_dir = template_dir
|
||||
self.openstack_release = openstack_release
|
||||
self.callback_f = callback_f
|
||||
self.setup_pebble_handler()
|
||||
|
||||
def setup_pebble_handler(self) -> None:
|
||||
"""Configure handler for pebble ready event."""
|
||||
prefix = self.container_name.replace('-', '_')
|
||||
pebble_ready_event = getattr(
|
||||
self.charm.on,
|
||||
f'{prefix}_pebble_ready')
|
||||
self.framework.observe(pebble_ready_event,
|
||||
self._on_service_pebble_ready)
|
||||
|
||||
def _on_service_pebble_ready(self,
|
||||
event: ops.charm.PebbleReadyEvent) -> None:
|
||||
"""Handle pebble ready event."""
|
||||
container = event.workload
|
||||
container.add_layer(
|
||||
self.service_name,
|
||||
self.get_layer(),
|
||||
combine=True)
|
||||
logger.debug(f'Plan: {container.get_plan()}')
|
||||
self.ready = True
|
||||
self._state.pebble_ready = True
|
||||
self.charm.configure_charm(event)
|
||||
|
||||
def write_config(self, context) -> None:
|
||||
"""Write configuration files into the container.
|
||||
|
||||
On the pre-condition that all relation adapters are ready
|
||||
for use, write all configuration files into the container
|
||||
so that the underlying service may be started.
|
||||
"""
|
||||
container = self.charm.unit.get_container(
|
||||
self.container_name)
|
||||
if container:
|
||||
sunbeam_templating.sidecar_config_render(
|
||||
[container],
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
context)
|
||||
self._state.config_pushed = True
|
||||
else:
|
||||
logger.debug(
|
||||
'Container not ready')
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
"""Pebble configuration layer for the container"""
|
||||
return {}
|
||||
|
||||
def init_service(self, context) -> None:
|
||||
"""Initialise service ready for use.
|
||||
|
||||
Write configuration files to the container and record
|
||||
that service is ready for us.
|
||||
"""
|
||||
self.write_config(context)
|
||||
self._state.service_ready = True
|
||||
|
||||
def default_container_configs(self) -> List[
|
||||
sunbeam_core.ContainerConfigFile]:
|
||||
"""Generate default container configurations.
|
||||
|
||||
These should be used by all inheriting classes and are
|
||||
automatically added to the list or container configurations
|
||||
provided during object instantiation.
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def pebble_ready(self) -> bool:
|
||||
"""Determine if pebble is running and ready for use."""
|
||||
return self._state.pebble_ready
|
||||
|
||||
@property
|
||||
def config_pushed(self) -> bool:
|
||||
"""Determine if configuration has been pushed to the container."""
|
||||
return self._state.config_pushed
|
||||
|
||||
@property
|
||||
def service_ready(self) -> bool:
|
||||
"""Determine whether the service the container provides is running."""
|
||||
return self._state.service_ready
|
||||
|
||||
|
||||
class WSGIPebbleHandler(PebbleHandler):
|
||||
"""WSGI oriented handler for a Pebble managed container."""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
container_name: str, service_name: str,
|
||||
container_configs: List[sunbeam_core.ContainerConfigFile],
|
||||
template_dir: str, openstack_release: str,
|
||||
callback_f: Callable,
|
||||
wsgi_service_name: str):
|
||||
super().__init__(charm, container_name, service_name,
|
||||
container_configs, template_dir, openstack_release,
|
||||
callback_f)
|
||||
self.wsgi_service_name = wsgi_service_name
|
||||
|
||||
def start_wsgi(self) -> None:
|
||||
"""Start WSGI service"""
|
||||
container = self.charm.unit.get_container(self.container_name)
|
||||
if not container:
|
||||
logger.debug(f'{self.container_name} container is not ready. '
|
||||
'Cannot start wgi service.')
|
||||
return
|
||||
service = container.get_service(self.wsgi_service_name)
|
||||
if service.is_running():
|
||||
container.stop(self.wsgi_service_name)
|
||||
|
||||
container.start(self.wsgi_service_name)
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
"""Apache WSGI service pebble layer
|
||||
|
||||
:returns: pebble layer configuration for wsgi service
|
||||
"""
|
||||
return {
|
||||
'summary': f'{self.service_name} layer',
|
||||
'description': 'pebble config layer for apache wsgi',
|
||||
'services': {
|
||||
f'{self.wsgi_service_name}': {
|
||||
'override': 'replace',
|
||||
'summary': f'{self.service_name} wsgi',
|
||||
'command': '/usr/sbin/apache2ctl -DFOREGROUND',
|
||||
'startup': 'disabled',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def init_service(self, context) -> None:
|
||||
"""Enable and start WSGI service"""
|
||||
container = self.charm.unit.get_container(self.container_name)
|
||||
self.write_config(context)
|
||||
try:
|
||||
sunbeam_cprocess.check_output(
|
||||
container,
|
||||
f'a2ensite {self.wsgi_service_name} && sleep 1')
|
||||
except sunbeam_cprocess.ContainerProcessError:
|
||||
logger.exception(
|
||||
f'Failed to enable {self.wsgi_service_name} site in apache')
|
||||
# ignore for now - pebble is raising an exited too quickly, but it
|
||||
# appears to work properly.
|
||||
self.start_wsgi()
|
||||
self._state.service_ready = True
|
||||
|
||||
@property
|
||||
def wsgi_conf(self) -> str:
|
||||
return f'/etc/apache2/sites-available/wsgi-{self.service_name}.conf'
|
||||
|
||||
def default_container_configs(self) -> List[
|
||||
sunbeam_core.ContainerConfigFile]:
|
||||
return [
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
self.wsgi_conf,
|
||||
'root',
|
||||
'root')]
|
@ -1,563 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2021 Billy Olsen
|
||||
# See LICENSE file for licensing details.
|
||||
#
|
||||
# Learn more at: https://juju.is/docs/sdk
|
||||
|
||||
"""Charm the service.
|
||||
|
||||
Refer to the following post for a quick-start guide that will help you
|
||||
develop a new k8s charm using the Operator Framework:
|
||||
|
||||
https://discourse.charmhub.io/t/4208
|
||||
"""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
|
||||
import advanced_sunbeam_openstack.adapters as sunbeam_adapters
|
||||
import advanced_sunbeam_openstack.templating as sunbeam_templating
|
||||
import advanced_sunbeam_openstack.cprocess as sunbeam_cprocess
|
||||
|
||||
import charms.nginx_ingress_integrator.v0.ingress as ingress
|
||||
import charms.mysql.v1.mysql as mysql
|
||||
|
||||
import ops.charm
|
||||
import ops.framework
|
||||
import ops.model
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import List, Tuple
|
||||
from ops_openstack.adapters import OpenStackOperRelationAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ContainerConfigFile = collections.namedtuple(
|
||||
'ContainerConfigFile',
|
||||
['container_names', 'path', 'user', 'group'])
|
||||
|
||||
|
||||
class PebbleHandler(ops.charm.Object):
|
||||
"""Base handler for Pebble based containers."""
|
||||
class OPSCharmContexts():
|
||||
|
||||
_state = ops.framework.StoredState()
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
container_name: str, service_name: str,
|
||||
container_configs: List[ContainerConfigFile],
|
||||
template_dir: str, openstack_release: str,
|
||||
adapters: List[OpenStackOperRelationAdapter],
|
||||
callback_f: Callable):
|
||||
super().__init__(charm, None)
|
||||
self._state.set_default(pebble_ready=False)
|
||||
self._state.set_default(config_pushed=False)
|
||||
self._state.set_default(service_ready=False)
|
||||
def __init__(self, charm):
|
||||
self.charm = charm
|
||||
self.container_name = container_name
|
||||
self.service_name = service_name
|
||||
self.container_configs = container_configs
|
||||
self.container_configs.extend(self.default_container_configs())
|
||||
self.template_dir = template_dir
|
||||
self.openstack_release = openstack_release
|
||||
self.adapters = adapters
|
||||
self.callback_f = callback_f
|
||||
self.setup_pebble_handler()
|
||||
self.namespaces = []
|
||||
|
||||
def setup_pebble_handler(self) -> None:
|
||||
"""Configure handler for pebble ready event."""
|
||||
prefix = self.container_name.replace('-', '_')
|
||||
pebble_ready_event = getattr(
|
||||
self.charm.on,
|
||||
f'{prefix}_pebble_ready')
|
||||
self.framework.observe(pebble_ready_event,
|
||||
self._on_service_pebble_ready)
|
||||
def add_relation_handler(self, handler):
|
||||
interface, relation_name = handler.get_interface()
|
||||
_ns = relation_name.replace("-", "_")
|
||||
self.namespaces.append(_ns)
|
||||
ctxt = handler.context()
|
||||
obj_name = ''.join([w.capitalize() for w in relation_name.split('-')])
|
||||
obj = collections.namedtuple(obj_name, ctxt.keys())(*ctxt.values())
|
||||
setattr(self, _ns, obj)
|
||||
|
||||
def _on_service_pebble_ready(self,
|
||||
event: ops.charm.PebbleReadyEvent) -> None:
|
||||
"""Handle pebble ready event."""
|
||||
container = event.workload
|
||||
container.add_layer(
|
||||
self.service_name,
|
||||
self.get_layer(),
|
||||
combine=True)
|
||||
logger.debug(f'Plan: {container.get_plan()}')
|
||||
self.ready = True
|
||||
self.charm.configure_charm(event)
|
||||
self._state.pebble_ready = True
|
||||
def add_config_contexts(self, config_adapters):
|
||||
for config_adapter in config_adapters:
|
||||
self.add_config_context(
|
||||
config_adapter,
|
||||
config_adapter.namespace)
|
||||
|
||||
def write_config(self) -> None:
|
||||
"""Write configuration files into the container.
|
||||
def add_config_context(self, config_adapter, namespace):
|
||||
self.namespaces.append(namespace)
|
||||
setattr(self, namespace, config_adapter)
|
||||
|
||||
On the pre-condition that all relation adapters are ready
|
||||
for use, write all configuration files into the container
|
||||
so that the underlying service may be started.
|
||||
def __iter__(self):
|
||||
"""
|
||||
for adapter in self.adapters:
|
||||
if not adapter[1].ready:
|
||||
logger.info("Adapter incomplete")
|
||||
return
|
||||
container = self.charm.unit.get_container(
|
||||
self.container_name)
|
||||
if container:
|
||||
sunbeam_templating.sidecar_config_render(
|
||||
[container],
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.adapters)
|
||||
self._state.config_pushed = True
|
||||
else:
|
||||
logger.debug(
|
||||
'Container not ready')
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
"""Pebble configuration layer for the container"""
|
||||
return {}
|
||||
|
||||
def init_service(self) -> None:
|
||||
"""Initialise service ready for use.
|
||||
|
||||
Write configuration files to the container and record
|
||||
that service is ready for us.
|
||||
Iterate over the relations presented to the charm.
|
||||
"""
|
||||
self.write_config()
|
||||
self._state.service_ready = True
|
||||
|
||||
def default_container_configs(self) -> List[ContainerConfigFile]:
|
||||
"""Generate default container configurations.
|
||||
|
||||
These should be used by all inheriting classes and are
|
||||
automatically added to the list or container configurations
|
||||
provided during object instantiation.
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def pebble_ready(self) -> bool:
|
||||
"""Determine if pebble is running and ready for use."""
|
||||
return self._state.pebble_ready
|
||||
|
||||
@property
|
||||
def config_pushed(self) -> bool:
|
||||
"""Determine if configuration has been pushed to the container."""
|
||||
return self._state.config_pushed
|
||||
|
||||
@property
|
||||
def service_ready(self) -> bool:
|
||||
"""Determine whether the service the container provides is running."""
|
||||
return self._state.service_ready
|
||||
|
||||
|
||||
class WSGIPebbleHandler(PebbleHandler):
|
||||
"""WSGI oriented handler for a Pebble managed container."""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
container_name: str, service_name: str,
|
||||
container_configs: List[ContainerConfigFile],
|
||||
template_dir: str, openstack_release: str,
|
||||
adapters: List[OpenStackOperRelationAdapter],
|
||||
callback_f: Callable,
|
||||
wsgi_service_name: str):
|
||||
super().__init__(charm, container_name, service_name,
|
||||
container_configs, template_dir, openstack_release,
|
||||
adapters, callback_f)
|
||||
self.wsgi_service_name = wsgi_service_name
|
||||
|
||||
def start_wsgi(self) -> None:
|
||||
"""Start WSGI service"""
|
||||
container = self.charm.unit.get_container(self.container_name)
|
||||
if not container:
|
||||
logger.debug(f'{self.container_name} container is not ready. '
|
||||
'Cannot start wgi service.')
|
||||
return
|
||||
service = container.get_service(self.wsgi_service_name)
|
||||
if service.is_running():
|
||||
container.stop(self.wsgi_service_name)
|
||||
|
||||
container.start(self.wsgi_service_name)
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
"""Apache WSGI service pebble layer
|
||||
|
||||
:returns: pebble layer configuration for wsgi service
|
||||
"""
|
||||
return {
|
||||
'summary': f'{self.service_name} layer',
|
||||
'description': 'pebble config layer for apache wsgi',
|
||||
'services': {
|
||||
f'{self.wsgi_service_name}': {
|
||||
'override': 'replace',
|
||||
'summary': f'{self.service_name} wsgi',
|
||||
'command': '/usr/sbin/apache2ctl -DFOREGROUND',
|
||||
'startup': 'disabled',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def init_service(self) -> None:
|
||||
"""Enable and start WSGI service"""
|
||||
container = self.charm.unit.get_container(self.container_name)
|
||||
self.write_config()
|
||||
try:
|
||||
sunbeam_cprocess.check_output(
|
||||
container,
|
||||
f'a2ensite {self.wsgi_service_name} && sleep 1')
|
||||
except sunbeam_cprocess.ContainerProcessError:
|
||||
logger.exception(
|
||||
f'Failed to enable {self.wsgi_service_name} site in apache')
|
||||
# ignore for now - pebble is raising an exited too quickly, but it
|
||||
# appears to work properly.
|
||||
self.start_wsgi()
|
||||
self._state.service_ready = True
|
||||
|
||||
@property
|
||||
def wsgi_conf(self) -> str:
|
||||
return f'/etc/apache2/sites-available/wsgi-{self.service_name}.conf'
|
||||
|
||||
def default_container_configs(self) -> List[ContainerConfigFile]:
|
||||
return [
|
||||
ContainerConfigFile(
|
||||
[self.container_name],
|
||||
self.wsgi_conf,
|
||||
'root',
|
||||
'root')]
|
||||
|
||||
|
||||
class RelationHandler(ops.charm.Object):
|
||||
"""Base handler class for relations"""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
relation_name: str, callback_f: Callable):
|
||||
super().__init__(charm, None)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback_f = callback_f
|
||||
self.interface = self.setup_event_handler()
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for the relation.
|
||||
|
||||
This method must be overridden in concrete class
|
||||
implementations.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_interface(self) -> Tuple[ops.charm.Object, str]:
|
||||
"""Returns the interface that this handler encapsulates.
|
||||
|
||||
This is a combination of the interface object and the
|
||||
name of the relation its wired into.
|
||||
"""
|
||||
return self.interface, self.relation_name
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Determine with the relation is ready for use."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class IngressHandler(RelationHandler):
|
||||
"""Handler for Ingress relations"""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
service_name: str,
|
||||
default_public_ingress_port: int,
|
||||
callback_f: Callable):
|
||||
self.default_public_ingress_port = default_public_ingress_port
|
||||
self.service_name = service_name
|
||||
super().__init__(charm, relation_name, callback_f)
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for an Ingress relation."""
|
||||
logger.debug('Setting up ingress event handler')
|
||||
interface = ingress.IngressRequires(
|
||||
self.charm,
|
||||
self.ingress_config)
|
||||
return interface
|
||||
|
||||
@property
|
||||
def ingress_config(self) -> dict:
|
||||
"""Ingress controller configuration dictionary."""
|
||||
# Most charms probably won't (or shouldn't) expose service-port
|
||||
# but use it if its there.
|
||||
port = self.model.config.get(
|
||||
'service-port',
|
||||
self.default_public_ingress_port)
|
||||
svc_hostname = self.model.config.get(
|
||||
'os-public-hostname',
|
||||
self.service_name)
|
||||
return {
|
||||
'service-hostname': svc_hostname,
|
||||
'service-name': self.charm.app.name,
|
||||
'service-port': port}
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
# Nothing to wait for
|
||||
return True
|
||||
|
||||
|
||||
class DBHandler(RelationHandler):
|
||||
"""Handler for DB relations"""
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for a MySQL relation."""
|
||||
logger.debug('Setting up DB event handler')
|
||||
db = mysql.MySQLConsumer(
|
||||
self.charm,
|
||||
self.relation_name,
|
||||
{"mysql": ">=8"})
|
||||
_rname = self.relation_name.replace('-', '_')
|
||||
db_relation_event = getattr(
|
||||
self.charm.on,
|
||||
f'{_rname}_relation_changed')
|
||||
self.framework.observe(db_relation_event,
|
||||
self._on_database_changed)
|
||||
return db
|
||||
|
||||
def _on_database_changed(self, event) -> None:
|
||||
"""Handles database change events."""
|
||||
databases = self.interface.databases()
|
||||
logger.info(f'Received databases: {databases}')
|
||||
|
||||
if not databases:
|
||||
logger.info('Requesting a new database...')
|
||||
# The mysql-k8s operator creates a database using the relation
|
||||
# information in the form of:
|
||||
# db_{relation_id}_{partial_uuid}_{name_suffix}
|
||||
# where name_suffix defaults to "". Specify it to the name of the
|
||||
# current app to make it somewhat understandable as to what this
|
||||
# database actually is for.
|
||||
# NOTE(wolsen): database name cannot contain a '-'
|
||||
name_suffix = self.charm.app.name.replace('-', '_')
|
||||
self.interface.new_database(name_suffix=name_suffix)
|
||||
return
|
||||
credentials = self.interface.credentials()
|
||||
# XXX Lets not log the credentials
|
||||
logger.info(f'Received credentials: {credentials}')
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Handler ready for use."""
|
||||
try:
|
||||
# Nothing to wait for
|
||||
return bool(self.interface.databases())
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
class OSBaseOperatorCharm(ops.charm.CharmBase):
|
||||
"""Base charms for OpenStack operators."""
|
||||
|
||||
_state = ops.framework.StoredState()
|
||||
|
||||
def __init__(self, framework, adapters=None):
|
||||
if adapters:
|
||||
self.adapters = adapters
|
||||
else:
|
||||
self.adapters = sunbeam_adapters.OPSRelationAdapters(self)
|
||||
super().__init__(framework)
|
||||
self.adapters.add_config_adapters(self.config_adapters)
|
||||
# Setup the observers for relationship events and pass the interfaces
|
||||
# to the adapter classes.
|
||||
self.relation_handlers = self.get_relation_handlers()
|
||||
for handler in self.relation_handlers:
|
||||
interface, relation_name = handler.get_interface()
|
||||
self.adapters.add_relation_adapter(
|
||||
interface,
|
||||
relation_name)
|
||||
self.pebble_handlers = self.get_pebble_handlers()
|
||||
self.framework.observe(self.on.config_changed,
|
||||
self._on_config_changed)
|
||||
|
||||
def get_relation_handlers(self) -> List[RelationHandler]:
|
||||
"""Relation handlers for the operator."""
|
||||
return []
|
||||
|
||||
def get_pebble_handlers(self) -> List[PebbleHandler]:
|
||||
"""Pebble handlers for the operator."""
|
||||
return [
|
||||
PebbleHandler(
|
||||
self,
|
||||
self.service_name,
|
||||
self.service_name,
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.adapters,
|
||||
self.configure_charm)]
|
||||
|
||||
def configure_charm(self, event) -> None:
|
||||
"""Configure containers when all dependencies are met.
|
||||
|
||||
Iterates over all Pebble handlers and writes configuration
|
||||
files if the handler is ready for use.
|
||||
"""
|
||||
for h in self.pebble_handlers:
|
||||
if h.ready:
|
||||
h.write_config()
|
||||
|
||||
@property
|
||||
def container_configs(self) -> List[ContainerConfigFile]:
|
||||
"""Container configuration files for the operator."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def config_adapters(self) -> List[sunbeam_adapters.CharmConfigAdapter]:
|
||||
"""Configuration adapters for the operator."""
|
||||
return [
|
||||
sunbeam_adapters.CharmConfigAdapter(self, 'options')]
|
||||
|
||||
@property
|
||||
def handler_prefix(self) -> str:
|
||||
"""Prefix for handlers??"""
|
||||
return self.service_name.replace('-', '_')
|
||||
|
||||
@property
|
||||
def container_names(self):
|
||||
"""Containers that form part of this service."""
|
||||
return [self.service_name]
|
||||
|
||||
@property
|
||||
def template_dir(self) -> str:
|
||||
"""Directory containing Jinja2 templates."""
|
||||
return 'src/templates'
|
||||
|
||||
def _on_config_changed(self, event):
|
||||
self.configure_charm(None)
|
||||
|
||||
def containers_ready(self) -> bool:
|
||||
"""Determine whether all containers are ready for configuration."""
|
||||
for ph in self.pebble_handlers:
|
||||
if not ph.service_ready:
|
||||
logger.info(f"Container incomplete: {ph.container_name}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def relation_handlers_ready(self) -> bool:
|
||||
"""Determine whether all relations are ready for use."""
|
||||
for handler in self.relation_handlers:
|
||||
if not handler.ready:
|
||||
logger.info(f"Relation {handler.relation_name} incomplete")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
|
||||
"""Base class for OpenStack API operators"""
|
||||
|
||||
def __init__(self, framework, adapters=None):
|
||||
if not adapters:
|
||||
adapters = sunbeam_adapters.APICharmAdapters(self)
|
||||
super().__init__(framework, adapters)
|
||||
self._state.set_default(db_ready=False)
|
||||
self._state.set_default(bootstrapped=False)
|
||||
|
||||
def get_pebble_handlers(self) -> List[PebbleHandler]:
|
||||
"""Pebble handlers for the service"""
|
||||
return [
|
||||
WSGIPebbleHandler(
|
||||
self,
|
||||
self.service_name,
|
||||
self.service_name,
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.adapters,
|
||||
self.configure_charm,
|
||||
f'wsgi-{self.service_name}')]
|
||||
|
||||
def get_relation_handlers(self) -> List[RelationHandler]:
|
||||
"""Relation handlers for the service."""
|
||||
self.db = DBHandler(
|
||||
self,
|
||||
f'{self.service_name}-db',
|
||||
self.configure_charm)
|
||||
self.ingress = IngressHandler(
|
||||
self,
|
||||
'ingress',
|
||||
self.service_name,
|
||||
self.default_public_ingress_port,
|
||||
self.configure_charm)
|
||||
return [self.db, self.ingress]
|
||||
|
||||
@property
|
||||
def container_configs(self) -> List[ContainerConfigFile]:
|
||||
"""Container configuration files for the service."""
|
||||
_cconfigs = super().container_configs
|
||||
_cconfigs.extend([
|
||||
ContainerConfigFile(
|
||||
[self.wsgi_container_name],
|
||||
self.service_conf,
|
||||
self.service_user,
|
||||
self.service_group)])
|
||||
return _cconfigs
|
||||
|
||||
@property
|
||||
def service_user(self) -> str:
|
||||
"""Service user file and directory ownership."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def service_group(self) -> str:
|
||||
"""Service group file and directory ownership."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def service_conf(self) -> str:
|
||||
"""Service default configuration file."""
|
||||
return f'/etc/{self.service_name}/{self.service_name}.conf'
|
||||
|
||||
@property
|
||||
def config_adapters(self) -> List[sunbeam_adapters.ConfigAdapter]:
|
||||
"""Generate list of configuration adapters for the charm."""
|
||||
_cadapters = super().config_adapters
|
||||
_cadapters.extend([
|
||||
sunbeam_adapters.WSGIWorkerConfigAdapter(self, 'wsgi_config')])
|
||||
return _cadapters
|
||||
|
||||
@property
|
||||
def wsgi_container_name(self) -> str:
|
||||
"""Name of the WSGI application container."""
|
||||
return self.service_name
|
||||
|
||||
@property
|
||||
def default_public_ingress_port(self) -> int:
|
||||
"""Port to use for ingress access to service."""
|
||||
raise NotImplementedError
|
||||
|
||||
def configure_charm(self, event) -> None:
|
||||
"""Catchall handler to cconfigure charm services."""
|
||||
if not self.relation_handlers_ready():
|
||||
logging.debug("Aborting charm relations not ready")
|
||||
return
|
||||
|
||||
for ph in self.pebble_handlers:
|
||||
if ph.pebble_ready:
|
||||
ph.init_service()
|
||||
|
||||
for ph in self.pebble_handlers:
|
||||
if not ph.service_ready:
|
||||
logging.debug("Aborting container service not ready")
|
||||
return
|
||||
|
||||
if not self.bootstrapped():
|
||||
self._do_bootstrap()
|
||||
|
||||
self.unit.status = ops.model.ActiveStatus()
|
||||
self._state.bootstrapped = True
|
||||
|
||||
def _do_bootstrap(self) -> None:
|
||||
"""Bootstrap the service ready for operation.
|
||||
|
||||
This method should be overridden as part of a concrete
|
||||
charm implementation
|
||||
"""
|
||||
pass
|
||||
|
||||
def bootstrapped(self) -> bool:
|
||||
"""Determine whether the service has been boostrapped."""
|
||||
return self._state.bootstrapped
|
||||
for namespace in self.namespaces:
|
||||
yield namespace, getattr(self, namespace)
|
||||
|
254
ops-sunbeam/advanced_sunbeam_openstack/relation_handlers.py
Normal file
254
ops-sunbeam/advanced_sunbeam_openstack/relation_handlers.py
Normal file
@ -0,0 +1,254 @@
|
||||
# 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.
|
||||
|
||||
"""Base classes for defining a charm using the Operator framework.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Tuple
|
||||
|
||||
import ops.charm
|
||||
|
||||
import charms.nginx_ingress_integrator.v0.ingress as ingress
|
||||
import charms.mysql.v1.mysql as mysql
|
||||
import charms.sunbeam_rabbitmq_operator.v0.amqp as sunbeam_amqp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RelationHandler(ops.charm.Object):
|
||||
"""Base handler class for relations
|
||||
|
||||
A relation handler is used to manage a charms interaction with a relation
|
||||
interface. This includes:
|
||||
|
||||
1) Registering handlers to process events from the interface. The last
|
||||
step of these handlers is to make a callback to a specified method
|
||||
within the charm `callback_f`
|
||||
2) Expose a `ready` property so the charm can check a relations readyness
|
||||
3) A `context` method which returns a dict which pulls together data
|
||||
recieved and sent on an interface.
|
||||
"""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
relation_name: str, callback_f: Callable):
|
||||
super().__init__(charm, None)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback_f = callback_f
|
||||
self.interface = self.setup_event_handler()
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for the relation.
|
||||
|
||||
This method must be overridden in concrete class
|
||||
implementations.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_interface(self) -> Tuple[ops.charm.Object, str]:
|
||||
"""Returns the interface that this handler encapsulates.
|
||||
|
||||
This is a combination of the interface object and the
|
||||
name of the relation its wired into.
|
||||
"""
|
||||
return self.interface, self.relation_name
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Determine with the relation is ready for use."""
|
||||
raise NotImplementedError
|
||||
|
||||
def context(self) -> dict:
|
||||
"""Pull together context for rendering templates."""
|
||||
return {}
|
||||
|
||||
|
||||
class IngressHandler(RelationHandler):
|
||||
"""Handler for Ingress relations"""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
service_name: str,
|
||||
default_public_ingress_port: int,
|
||||
callback_f: Callable):
|
||||
self.default_public_ingress_port = default_public_ingress_port
|
||||
self.service_name = service_name
|
||||
super().__init__(charm, relation_name, callback_f)
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for an Ingress relation."""
|
||||
logger.debug('Setting up ingress event handler')
|
||||
interface = ingress.IngressRequires(
|
||||
self.charm,
|
||||
self.ingress_config)
|
||||
return interface
|
||||
|
||||
@property
|
||||
def ingress_config(self) -> dict:
|
||||
"""Ingress controller configuration dictionary."""
|
||||
# Most charms probably won't (or shouldn't) expose service-port
|
||||
# but use it if its there.
|
||||
port = self.model.config.get(
|
||||
'service-port',
|
||||
self.default_public_ingress_port)
|
||||
svc_hostname = self.model.config.get(
|
||||
'os-public-hostname',
|
||||
self.service_name)
|
||||
return {
|
||||
'service-hostname': svc_hostname,
|
||||
'service-name': self.charm.app.name,
|
||||
'service-port': port}
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
# Nothing to wait for
|
||||
return True
|
||||
|
||||
def context(self):
|
||||
return {}
|
||||
|
||||
|
||||
class DBHandler(RelationHandler):
|
||||
"""Handler for DB relations"""
|
||||
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for a MySQL relation."""
|
||||
logger.debug('Setting up DB event handler')
|
||||
db = mysql.MySQLConsumer(
|
||||
self.charm,
|
||||
self.relation_name,
|
||||
{"mysql": ">=8"})
|
||||
_rname = self.relation_name.replace('-', '_')
|
||||
db_relation_event = getattr(
|
||||
self.charm.on,
|
||||
f'{_rname}_relation_changed')
|
||||
self.framework.observe(db_relation_event,
|
||||
self._on_database_changed)
|
||||
return db
|
||||
|
||||
def _on_database_changed(self, event) -> None:
|
||||
"""Handles database change events."""
|
||||
databases = self.interface.databases()
|
||||
logger.info(f'Received databases: {databases}')
|
||||
|
||||
if not databases:
|
||||
logger.info('Requesting a new database...')
|
||||
# The mysql-k8s operator creates a database using the relation
|
||||
# information in the form of:
|
||||
# db_{relation_id}_{partial_uuid}_{name_suffix}
|
||||
# where name_suffix defaults to "". Specify it to the name of the
|
||||
# current app to make it somewhat understandable as to what this
|
||||
# database actually is for.
|
||||
# NOTE(wolsen): database name cannot contain a '-'
|
||||
name_suffix = self.charm.app.name.replace('-', '_')
|
||||
self.interface.new_database(name_suffix=name_suffix)
|
||||
return
|
||||
credentials = self.interface.credentials()
|
||||
# XXX Lets not log the credentials
|
||||
logger.info(f'Received credentials: {credentials}')
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Handler ready for use."""
|
||||
try:
|
||||
# Nothing to wait for
|
||||
return bool(self.interface.databases())
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def context(self):
|
||||
try:
|
||||
databases = self.interface.databases()
|
||||
except AttributeError:
|
||||
return {}
|
||||
if not databases:
|
||||
return {}
|
||||
ctxt = {
|
||||
'database': self.interface.databases()[0],
|
||||
'database_host': self.interface.credentials().get('address'),
|
||||
'database_password': self.interface.credentials().get('password'),
|
||||
'database_user': self.interface.credentials().get('username'),
|
||||
'database_type': 'mysql+pymysql'}
|
||||
return ctxt
|
||||
|
||||
|
||||
class AMQPHandler(RelationHandler):
|
||||
|
||||
DEFAULT_PORT = "5672"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f,
|
||||
username: str,
|
||||
vhost: int,
|
||||
):
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
super().__init__(charm, relation_name, callback_f)
|
||||
|
||||
def setup_event_handler(self):
|
||||
"""Configure event handlers for an AMQP relation."""
|
||||
logger.debug("Setting up AMQP event handler")
|
||||
amqp = sunbeam_amqp.AMQPRequires(
|
||||
self.charm, self.relation_name, self.username, self.vhost
|
||||
)
|
||||
self.framework.observe(amqp.on.ready, self._on_amqp_ready)
|
||||
return amqp
|
||||
|
||||
def _on_amqp_ready(self, event) -> None:
|
||||
"""Handles AMQP change events."""
|
||||
# Ready is only emitted when the interface considers
|
||||
# that the relation is complete (indicated by a password)
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Handler ready for use."""
|
||||
try:
|
||||
return bool(self.interface.password)
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def context(self):
|
||||
try:
|
||||
hosts = self.interface.hostnames
|
||||
except AttributeError:
|
||||
return {}
|
||||
if not hosts:
|
||||
return {}
|
||||
hosts = ','.join(hosts)
|
||||
port = self.interface.ssl_port or self.DEFAULT_PORT
|
||||
username = self.interface.username
|
||||
password = self.interface.password
|
||||
transport_url_hosts = ','.join([
|
||||
"{}:{}@{}:{}".format(username,
|
||||
password,
|
||||
host_, # TODO deal with IPv6
|
||||
port)
|
||||
for host_ in self.interface.hostnames
|
||||
])
|
||||
transport_url = "rabbit://{}/{}".format(
|
||||
transport_url_hosts,
|
||||
self.vhost)
|
||||
ctxt = {
|
||||
'port': port,
|
||||
'hosts': hosts,
|
||||
'transport_url': transport_url}
|
||||
return ctxt
|
@ -15,20 +15,26 @@
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import io
|
||||
import json
|
||||
from mock import patch
|
||||
import os
|
||||
import tempfile
|
||||
from mock import ANY, patch
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
sys.path.append('lib') # noqa
|
||||
sys.path.append('src') # noqa
|
||||
|
||||
from ops.testing import Harness
|
||||
from ops import framework, model
|
||||
|
||||
import advanced_sunbeam_openstack.core as core
|
||||
from ops.testing import Harness, _TestingModelBackend, _TestingPebbleClient
|
||||
|
||||
import advanced_sunbeam_openstack.charm as sunbeam_charm
|
||||
|
||||
CHARM_CONFIG = {
|
||||
'debug': 'true'}
|
||||
|
||||
CHARM_METADATA = '''
|
||||
name: my-service
|
||||
version: 3
|
||||
@ -42,13 +48,45 @@ tags:
|
||||
|
||||
subordinate: false
|
||||
|
||||
containers:
|
||||
my-service:
|
||||
resource: mysvc-image
|
||||
mounts:
|
||||
- storage: db
|
||||
location: /var/lib/mysvc
|
||||
|
||||
storage:
|
||||
logs:
|
||||
type: filesystem
|
||||
db:
|
||||
type: filesystem
|
||||
|
||||
resources:
|
||||
mysvc-image:
|
||||
type: oci-image
|
||||
'''
|
||||
|
||||
API_CHARM_METADATA = '''
|
||||
name: my-service
|
||||
version: 3
|
||||
bases:
|
||||
- name: ubuntu
|
||||
channel: 20.04/stable
|
||||
tags:
|
||||
- openstack
|
||||
- identity
|
||||
- misc
|
||||
|
||||
subordinate: false
|
||||
|
||||
requires:
|
||||
my-service-db:
|
||||
interface: mysql_datastore
|
||||
limit: 1
|
||||
ingress:
|
||||
interface: ingress
|
||||
|
||||
amqp:
|
||||
interface: rabbitmq
|
||||
|
||||
peers:
|
||||
peers:
|
||||
@ -87,12 +125,18 @@ class CharmTestCase(unittest.TestCase):
|
||||
self.addCleanup(_m.stop)
|
||||
return mock
|
||||
|
||||
def patch_obj(self, obj, method):
|
||||
_m = patch.object(obj, method)
|
||||
mock = _m.start()
|
||||
self.addCleanup(_m.stop)
|
||||
return mock
|
||||
|
||||
def patch_all(self):
|
||||
for method in self.patches:
|
||||
setattr(self, method, self.patch(method))
|
||||
|
||||
|
||||
class MyCharm(core.OSBaseOperatorCharm):
|
||||
class MyCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
|
||||
openstack_release = 'diablo'
|
||||
service_name = 'my-service'
|
||||
@ -100,11 +144,13 @@ class MyCharm(core.OSBaseOperatorCharm):
|
||||
def __init__(self, framework):
|
||||
super().__init__(framework)
|
||||
self.seen_events = []
|
||||
self.render_calls = []
|
||||
|
||||
def _log_event(self, event):
|
||||
self.seen_events.append(type(event).__name__)
|
||||
|
||||
def _on_service_pebble_ready(self, event):
|
||||
super()._on_service_pebble_ready(event)
|
||||
self._log_event(event)
|
||||
|
||||
def _on_config_changed(self, event):
|
||||
@ -114,10 +160,6 @@ class MyCharm(core.OSBaseOperatorCharm):
|
||||
super().configure_charm(event)
|
||||
self._log_event(event)
|
||||
|
||||
def _configure_charm(self, event):
|
||||
super()._configure_charm(event)
|
||||
self._log_event(event)
|
||||
|
||||
@property
|
||||
def public_ingress_port(self):
|
||||
return 789
|
||||
@ -126,16 +168,14 @@ class MyCharm(core.OSBaseOperatorCharm):
|
||||
class TestOSBaseOperatorCharm(CharmTestCase):
|
||||
|
||||
PATCHES = [
|
||||
'sunbeam_templating'
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp(core, self.PATCHES)
|
||||
super().setUp(sunbeam_charm, self.PATCHES)
|
||||
self.harness = Harness(
|
||||
MyCharm,
|
||||
meta=CHARM_METADATA
|
||||
)
|
||||
|
||||
self.harness.update_config(CHARM_CONFIG)
|
||||
self.harness.begin()
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
@ -145,19 +185,21 @@ class TestOSBaseOperatorCharm(CharmTestCase):
|
||||
# Emit the PebbleReadyEvent
|
||||
self.harness.charm.on.my_service_pebble_ready.emit(container)
|
||||
|
||||
def test_pebble_ready_handler(self):
|
||||
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
|
||||
def test_pebble_ready_handler(self, _renderer):
|
||||
self.assertEqual(self.harness.charm.seen_events, [])
|
||||
self.set_pebble_ready()
|
||||
self.assertEqual(self.harness.charm.seen_events, ['PebbleReadyEvent'])
|
||||
|
||||
def test_write_config(self):
|
||||
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
|
||||
def test_write_config(self, _renderer):
|
||||
self.set_pebble_ready()
|
||||
self.sunbeam_templating.sidecar_config_render.assert_called_once_with(
|
||||
_renderer.assert_called_once_with(
|
||||
[self.harness.model.unit.get_container("my-service")],
|
||||
[],
|
||||
'src/templates',
|
||||
'diablo',
|
||||
self.harness.charm.adapters)
|
||||
ANY)
|
||||
|
||||
def test_handler_prefix(self):
|
||||
self.assertEqual(
|
||||
@ -178,8 +220,15 @@ class TestOSBaseOperatorCharm(CharmTestCase):
|
||||
self.assertTrue(
|
||||
self.harness.charm.relation_handlers_ready())
|
||||
|
||||
TEMPLATE_CONTENTS = """
|
||||
{{ wsgi_config.wsgi_admin_script }}
|
||||
{{ my_service_db.database_password }}
|
||||
{{ options.debug }}
|
||||
{{ amqp.transport_url }}
|
||||
"""
|
||||
|
||||
class MyAPICharm(core.OSBaseOperatorAPICharm):
|
||||
|
||||
class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
openstack_release = 'diablo'
|
||||
service_name = 'my-service'
|
||||
wsgi_admin_script = '/bin/wsgi_admin'
|
||||
@ -188,8 +237,19 @@ class MyAPICharm(core.OSBaseOperatorAPICharm):
|
||||
def __init__(self, framework):
|
||||
self.seen_events = []
|
||||
self.render_calls = []
|
||||
self._template_dir = self._setup_templates()
|
||||
super().__init__(framework)
|
||||
|
||||
def _setup_templates(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
_template_dir = f'{tmpdir}/templates'
|
||||
os.mkdir(_template_dir)
|
||||
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
|
||||
f.write(TEMPLATE_CONTENTS)
|
||||
with open(f'{_template_dir}/wsgi-my-service.conf.j2', 'w') as f:
|
||||
f.write(TEMPLATE_CONTENTS)
|
||||
return _template_dir
|
||||
|
||||
def _log_event(self, event):
|
||||
self.seen_events.append(type(event).__name__)
|
||||
|
||||
@ -204,26 +264,97 @@ class MyAPICharm(core.OSBaseOperatorAPICharm):
|
||||
def default_public_ingress_port(self):
|
||||
return 789
|
||||
|
||||
@property
|
||||
def template_dir(self):
|
||||
return self._template_dir
|
||||
|
||||
|
||||
class TestOSBaseOperatorAPICharm(CharmTestCase):
|
||||
|
||||
PATCHES = [
|
||||
'sunbeam_templating',
|
||||
'sunbeam_cprocess',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp(core, self.PATCHES)
|
||||
self.sunbeam_cprocess.ContainerProcessError = Exception
|
||||
container_calls = {
|
||||
'push': {},
|
||||
'pull': [],
|
||||
'remove_path': []}
|
||||
|
||||
super().setUp(sunbeam_charm, self.PATCHES)
|
||||
|
||||
class _LYTestingPebbleClient(_TestingPebbleClient):
|
||||
|
||||
def push(
|
||||
self, path, source, *,
|
||||
encoding='utf-8', make_dirs=False, permissions=None,
|
||||
user_id=None, user=None, group_id=None, group=None):
|
||||
container_calls['push'][path] = {
|
||||
'source': source,
|
||||
'permissions': permissions,
|
||||
'user': user,
|
||||
'group': group}
|
||||
|
||||
def pull(self, path, *, encoding='utf-8'):
|
||||
container_calls['pull'].append(path)
|
||||
reader = io.StringIO("0")
|
||||
return reader
|
||||
|
||||
def remove_path(self, path, *, recursive=False):
|
||||
container_calls['remove_path'].append(path)
|
||||
|
||||
class _LYTestingModelBackend(_TestingModelBackend):
|
||||
|
||||
def get_pebble(self, socket_path: str):
|
||||
client = self._pebble_clients.get(socket_path, None)
|
||||
if client is None:
|
||||
client = _LYTestingPebbleClient(self)
|
||||
self._pebble_clients[socket_path] = client
|
||||
return client
|
||||
|
||||
self.container_calls = container_calls
|
||||
# self.sunbeam_cprocess.ContainerProcessError = Exception
|
||||
self.harness = Harness(
|
||||
MyAPICharm,
|
||||
meta=CHARM_METADATA
|
||||
meta=API_CHARM_METADATA
|
||||
)
|
||||
self.harness._backend = _LYTestingModelBackend(
|
||||
self.harness._unit_name, self.harness._meta)
|
||||
self.harness._model = model.Model(
|
||||
self.harness._meta,
|
||||
self.harness._backend)
|
||||
self.harness._framework = framework.Framework(
|
||||
":memory:",
|
||||
self.harness._charm_dir,
|
||||
self.harness._meta,
|
||||
self.harness._model)
|
||||
# END Workaround
|
||||
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
self.harness.update_config(CHARM_CONFIG)
|
||||
self.harness.begin()
|
||||
|
||||
def add_base_amqp_relation(self):
|
||||
rel_id = self.harness.add_relation('amqp', 'rabbitmq')
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'rabbitmq/0')
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'rabbitmq/0')
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'rabbitmq/0',
|
||||
{'ingress-address': '10.0.0.13'})
|
||||
return rel_id
|
||||
|
||||
def add_amqp_relation_credentials(self, rel_id):
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'rabbitmq',
|
||||
{
|
||||
'hostname': 'rabbithost1.local',
|
||||
'password': 'rabbit.pass'})
|
||||
|
||||
def add_base_db_relation(self):
|
||||
rel_id = self.harness.add_relation('my-service-db', 'mysql')
|
||||
self.harness.add_relation_unit(
|
||||
@ -256,27 +387,34 @@ class TestOSBaseOperatorAPICharm(CharmTestCase):
|
||||
def test_write_config(self):
|
||||
self.harness.set_leader()
|
||||
self.set_pebble_ready()
|
||||
rel_id = self.add_base_db_relation()
|
||||
self.add_db_relation_credentials(rel_id)
|
||||
self.sunbeam_templating.sidecar_config_render.assert_called_once_with(
|
||||
[self.harness.model.unit.get_container("my-service")],
|
||||
[
|
||||
core.ContainerConfigFile(
|
||||
container_names=['my-service'],
|
||||
path=('/etc/my-service/my-service.conf'),
|
||||
user='my-service',
|
||||
group='my-service'),
|
||||
core.ContainerConfigFile(
|
||||
container_names=['my-service'],
|
||||
path=('/etc/apache2/sites-available/'
|
||||
'wsgi-my-service.conf'),
|
||||
user='root',
|
||||
group='root')],
|
||||
'src/templates',
|
||||
'diablo',
|
||||
self.harness.charm.adapters)
|
||||
db_rel_id = self.add_base_db_relation()
|
||||
self.add_db_relation_credentials(db_rel_id)
|
||||
amqp_rel_id = self.add_base_amqp_relation()
|
||||
self.add_amqp_relation_credentials(amqp_rel_id)
|
||||
expect_entries = [
|
||||
'/bin/wsgi_admin',
|
||||
'hardpassword',
|
||||
'true',
|
||||
'rabbit://my-service:rabbit.pass@10.0.0.13:5672/my-service']
|
||||
expect_string = '\n' + '\n'.join(expect_entries)
|
||||
self.assertEqual(
|
||||
self.container_calls['push']['/etc/my-service/my-service.conf'],
|
||||
{
|
||||
'group': 'my-service',
|
||||
'permissions': None,
|
||||
'source': expect_string,
|
||||
'user': 'my-service'})
|
||||
self.assertEqual(
|
||||
self.container_calls['push'][
|
||||
'/etc/apache2/sites-available/wsgi-my-service.conf'],
|
||||
{
|
||||
'group': 'root',
|
||||
'permissions': None,
|
||||
'source': expect_string,
|
||||
'user': 'root'})
|
||||
|
||||
def test__on_database_changed(self):
|
||||
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
|
||||
def test__on_database_changed(self, _renderer):
|
||||
self.harness.set_leader()
|
||||
self.set_pebble_ready()
|
||||
rel_id = self.add_base_db_relation()
|
||||
@ -287,17 +425,18 @@ class TestOSBaseOperatorAPICharm(CharmTestCase):
|
||||
requested_db = json.loads(rel_data['databases'])[0]
|
||||
self.assertRegex(requested_db, r'^db_.*my_service$')
|
||||
|
||||
def test_DBAdapter(self):
|
||||
def test_contexts(self):
|
||||
self.harness.set_leader()
|
||||
self.set_pebble_ready()
|
||||
rel_id = self.add_base_db_relation()
|
||||
self.add_db_relation_credentials(rel_id)
|
||||
contexts = self.harness.charm.contexts()
|
||||
self.assertEqual(
|
||||
self.harness.charm.adapters.wsgi_config.wsgi_admin_script,
|
||||
contexts.wsgi_config.wsgi_admin_script,
|
||||
'/bin/wsgi_admin')
|
||||
self.assertEqual(
|
||||
self.harness.charm.adapters.my_service_db.database_password,
|
||||
contexts.my_service_db.database_password,
|
||||
'hardpassword')
|
||||
self.assertEqual(
|
||||
self.harness.charm.adapters.options.debug,
|
||||
contexts.options.debug,
|
||||
'true')
|
||||
|
Loading…
Reference in New Issue
Block a user