diff --git a/ops-sunbeam/advanced_sunbeam_openstack/core.py b/ops-sunbeam/advanced_sunbeam_openstack/core.py index 544be8c7..02423a57 100644 --- a/ops-sunbeam/advanced_sunbeam_openstack/core.py +++ b/ops-sunbeam/advanced_sunbeam_openstack/core.py @@ -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