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:
Liam Young 2021-10-07 10:19:59 +01:00
parent e2f63e897d
commit 15a9842bd2
7 changed files with 997 additions and 772 deletions

View File

@ -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

View 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

View 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}

View 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')]

View File

@ -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)

View 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

View File

@ -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')