Merge pull request #3 from javacruft/add-type-hints

[chore] Add docstrings and typing to core module.
This commit is contained in:
Liam Young
2021-10-06 13:24:47 +01:00
committed by GitHub

View File

@@ -26,6 +26,10 @@ 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__)
@@ -35,12 +39,16 @@ ContainerConfigFile = collections.namedtuple(
class PebbleHandler(ops.charm.Object):
"""Base handler for Pebble based containers."""
_state = ops.framework.StoredState()
def __init__(self, charm, container_name, service_name,
container_configs, template_dir, openstack_release,
adapters, callback_f):
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)
@@ -56,7 +64,8 @@ class PebbleHandler(ops.charm.Object):
self.callback_f = callback_f
self.setup_pebble_handler()
def setup_pebble_handler(self):
def setup_pebble_handler(self) -> None:
"""Configure handler for pebble ready event."""
prefix = self.container_name.replace('-', '_')
pebble_ready_event = getattr(
self.charm.on,
@@ -66,6 +75,7 @@ class PebbleHandler(ops.charm.Object):
def _on_service_pebble_ready(self,
event: ops.charm.PebbleReadyEvent) -> None:
"""Handle pebble ready event."""
container = event.workload
container.add_layer(
self.service_name,
@@ -76,7 +86,13 @@ class PebbleHandler(ops.charm.Object):
self.charm.configure_charm(event)
self._state.pebble_ready = True
def write_config(self):
def write_config(self) -> 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.
"""
for adapter in self.adapters:
if not adapter[1].ready:
logger.info("Adapter incomplete")
@@ -95,40 +111,61 @@ class PebbleHandler(ops.charm.Object):
logger.debug(
'Container not ready')
def get_layer(self):
def get_layer(self) -> dict:
"""Pebble configuration layer for the container"""
return {}
def init_service(self):
def init_service(self) -> None:
"""Initialise service ready for use.
Write configuration files to the container and record
that service is ready for us.
"""
self.write_config()
self._state.service_ready = True
def default_container_configs(self):
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):
def pebble_ready(self) -> bool:
"""Determine if pebble is running and ready for use."""
return self._state.pebble_ready
@property
def config_pushed(self):
def config_pushed(self) -> bool:
"""Determine if configuration has been pushed to the container."""
return self._state.config_pushed
@property
def service_ready(self):
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, container_name, service_name, container_configs,
template_dir, openstack_release, adapters, callback_f,
wsgi_service_name):
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):
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. '
@@ -140,11 +177,10 @@ class WSGIPebbleHandler(PebbleHandler):
container.start(self.wsgi_service_name)
def get_layer(self):
"""Apache WSGI service
def get_layer(self) -> dict:
"""Apache WSGI service pebble layer
:returns: pebble layer configuration for wsgi services
:rtype: dict
:returns: pebble layer configuration for wsgi service
"""
return {
'summary': f'{self.service_name} layer',
@@ -159,7 +195,8 @@ class WSGIPebbleHandler(PebbleHandler):
},
}
def init_service(self):
def init_service(self) -> None:
"""Enable and start WSGI service"""
container = self.charm.unit.get_container(self.container_name)
self.write_config()
try:
@@ -175,10 +212,10 @@ class WSGIPebbleHandler(PebbleHandler):
self._state.service_ready = True
@property
def wsgi_conf(self):
def wsgi_conf(self) -> str:
return f'/etc/apache2/sites-available/wsgi-{self.service_name}.conf'
def default_container_configs(self):
def default_container_configs(self) -> List[ContainerConfigFile]:
return [
ContainerConfigFile(
[self.container_name],
@@ -188,34 +225,52 @@ class WSGIPebbleHandler(PebbleHandler):
class RelationHandler(ops.charm.Object):
"""Base handler class for relations"""
def __init__(self, charm, relation_name, callback_f):
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):
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):
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):
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, relation_name, service_name,
default_public_ingress_port, callback_f):
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):
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,
@@ -223,7 +278,8 @@ class IngressHandler(RelationHandler):
return interface
@property
def ingress_config(self):
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(
@@ -238,14 +294,16 @@ class IngressHandler(RelationHandler):
'service-port': port}
@property
def ready(self):
def ready(self) -> bool:
# Nothing to wait for
return True
class DBHandler(RelationHandler):
"""Handler for DB relations"""
def setup_event_handler(self):
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,
@@ -282,7 +340,8 @@ class DBHandler(RelationHandler):
self.callback_f(event)
@property
def ready(self):
def ready(self) -> bool:
"""Handler ready for use."""
try:
# Nothing to wait for
return bool(self.interface.databases())
@@ -291,6 +350,8 @@ class DBHandler(RelationHandler):
class OSBaseOperatorCharm(ops.charm.CharmBase):
"""Base charms for OpenStack operators."""
_state = ops.framework.StoredState()
def __init__(self, framework, adapters=None):
@@ -312,10 +373,12 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
self.framework.observe(self.on.config_changed,
self._on_config_changed)
def get_relation_handlers(self):
def get_relation_handlers(self) -> List[RelationHandler]:
"""Relation handlers for the operator."""
return []
def get_pebble_handlers(self):
def get_pebble_handlers(self) -> List[PebbleHandler]:
"""Pebble handlers for the operator."""
return [
PebbleHandler(
self,
@@ -327,53 +390,64 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
self.adapters,
self.configure_charm)]
def configure_charm(self, event):
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):
def container_configs(self) -> List[ContainerConfigFile]:
"""Container configuration files for the operator."""
return []
@property
def config_adapters(self):
def config_adapters(self) -> List[sunbeam_adapters.CharmConfigAdapter]:
"""Configuration adapters for the operator."""
return [
sunbeam_adapters.CharmConfigAdapter(self, 'options')]
@property
def handler_prefix(self):
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):
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):
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("Container incomplete")
logger.info(f"Container incomplete: {ph.container_name}")
return False
return True
def relation_handlers_ready(self):
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("Relation {} incomplete".format(
handler.relation_name))
logger.info(f"Relation {handler.relation_name} incomplete")
return False
return True
class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
_state = ops.framework.StoredState()
"""Base class for OpenStack API operators"""
def __init__(self, framework, adapters=None):
if not adapters:
@@ -382,7 +456,8 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
self._state.set_default(db_ready=False)
self._state.set_default(bootstrapped=False)
def get_pebble_handlers(self):
def get_pebble_handlers(self) -> List[PebbleHandler]:
"""Pebble handlers for the service"""
return [
WSGIPebbleHandler(
self,
@@ -395,7 +470,8 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
self.configure_charm,
f'wsgi-{self.service_name}')]
def get_relation_handlers(self):
def get_relation_handlers(self) -> List[RelationHandler]:
"""Relation handlers for the service."""
self.db = DBHandler(
self,
f'{self.service_name}-db',
@@ -409,7 +485,8 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
return [self.db, self.ingress]
@property
def container_configs(self):
def container_configs(self) -> List[ContainerConfigFile]:
"""Container configuration files for the service."""
_cconfigs = super().container_configs
_cconfigs.extend([
ContainerConfigFile(
@@ -420,33 +497,40 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
return _cconfigs
@property
def service_user(self):
def service_user(self) -> str:
"""Service user file and directory ownership."""
return self.service_name
@property
def service_group(self):
def service_group(self) -> str:
"""Service group file and directory ownership."""
return self.service_name
@property
def service_conf(self):
def service_conf(self) -> str:
"""Service default configuration file."""
return f'/etc/{self.service_name}/{self.service_name}.conf'
@property
def config_adapters(self):
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):
def wsgi_container_name(self) -> str:
"""Name of the WSGI application container."""
return self.service_name
@property
def default_public_ingress_port(self):
def default_public_ingress_port(self) -> int:
"""Port to use for ingress access to service."""
raise NotImplementedError
def configure_charm(self, event):
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
@@ -466,14 +550,14 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharm):
self.unit.status = ops.model.ActiveStatus()
self._state.bootstrapped = True
def _do_bootstrap(self):
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):
"""Returns True if the instance is bootstrapped.
:returns: True if the keystone service has been bootstrapped,
False otherwise
:rtype: bool
"""
def bootstrapped(self) -> bool:
"""Determine whether the service has been boostrapped."""
return self._state.bootstrapped