X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fpol%2Fsrc%2Fcharm.py;h=94f6ecb89573ac7333450443ebabd35c13a5c8c5;hb=4a46c6ee105522c9018e977977542e4bee50133c;hp=649e15bb121a3db639d47082765903cfa9069a48;hpb=cab58cb714924d6a6f2d4bc83dcc29445cf84bf4;p=osm%2Fdevops.git diff --git a/installers/charm/pol/src/charm.py b/installers/charm/pol/src/charm.py index 649e15bb..94f6ecb8 100755 --- a/installers/charm/pol/src/charm.py +++ b/installers/charm/pol/src/charm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2020 Canonical Ltd. +# 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 @@ -20,211 +20,216 @@ # osm-charmers@lists.launchpad.net ## +# pylint: disable=E0213 + + import logging -from pydantic import ValidationError -from typing import Any, Dict, NoReturn +import re +from typing import NoReturn, Optional -from ops.charm import CharmBase, CharmEvents -from ops.framework import EventBase, EventSource, StoredState +from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus -from oci_image import OCIImageResource, OCIImageResourceError +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.interfaces.mongo import MongoClient +from opslib.osm.interfaces.mysql import MysqlClient +from opslib.osm.pod import ( + ContainerV3Builder, + PodRestartPolicy, + PodSpecV3Builder, +) +from opslib.osm.validator import ModelValidator, validator -from pod_spec import make_pod_spec logger = logging.getLogger(__name__) +PORT = 9999 +DEFAULT_MYSQL_DATABASE = "pol" + + +class ConfigModel(ModelValidator): + log_level: str + mongodb_uri: Optional[str] + mysql_uri: Optional[str] + image_pull_policy: str + debug_mode: bool + security_context: bool + + @validator("log_level") + def validate_log_level(cls, v): + if v not in {"INFO", "DEBUG"}: + raise ValueError("value must be INFO or DEBUG") + return v + + @validator("mongoddb_uri") + def validate_mongodb_uri(cls, v): + if v and not v.startswith("mongodb://"): + raise ValueError("mongodb_uri is not properly formed") + return v + + @validator("mysql_uri") + def validate_mysql_uri(cls, v): + pattern = re.compile("^mysql:\/\/.*:.*@.*:\d+\/.*$") # noqa: W605 + if v and not pattern.search(v): + raise ValueError("mysql_uri is not properly formed") + return v + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] -class ConfigurePodEvent(EventBase): - """Configure Pod event""" - - pass - - -class PolEvents(CharmEvents): - """POL Events""" - - configure_pod = EventSource(ConfigurePodEvent) - - -class PolCharm(CharmBase): - """POL Charm.""" - state = StoredState() - on = PolEvents() +class PolCharm(CharmedOsmBase): + on = KafkaEvents() def __init__(self, *args) -> NoReturn: - """POL Charm constructor.""" - super().__init__(*args) - - # Internal state initialization - self.state.set_default(pod_spec=None) - - # Message bus data initialization - self.state.set_default(message_host=None) - self.state.set_default(message_port=None) - - # Database data initialization - self.state.set_default(database_uri=None) - - self.image = OCIImageResource(self, "image") - - # Registering regular events - self.framework.observe(self.on.start, self.configure_pod) - self.framework.observe(self.on.config_changed, self.configure_pod) - self.framework.observe(self.on.upgrade_charm, self.configure_pod) + super().__init__( + *args, + oci_image="image", + vscode_workspace=VSCODE_WORKSPACE, + ) + if self.config.get("debug_mode"): + self.enable_debug_mode( + pubkey=self.config.get("debug_pubkey"), + hostpaths={ + "POL": { + "hostpath": self.config.get("debug_pol_local_path"), + "container-path": "/usr/lib/python3/dist-packages/osm_policy_module", + }, + "osm_common": { + "hostpath": self.config.get("debug_common_local_path"), + "container-path": "/usr/lib/python3/dist-packages/osm_common", + }, + }, + ) + self.kafka = KafkaRequires(self) + self.framework.observe(self.on.kafka_available, self.configure_pod) + self.framework.observe(self.on.kafka_broken, self.configure_pod) + + self.mongodb_client = MongoClient(self, "mongodb") + self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod) + self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod) + + self.mysql_client = MysqlClient(self, "mysql") + self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod) + self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod) + + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] + + if not self.kafka.host or not self.kafka.port: + missing_relations.append("kafka") + if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit(): + missing_relations.append("mongodb") + if not config.mysql_uri and self.mysql_client.is_missing_data_in_unit(): + missing_relations.append("mysql") + if missing_relations: + raise RelationsMissing(missing_relations) + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + + if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit(): + raise Exception("Mongodb data cannot be provided via config and relation") + if config.mysql_uri and not self.mysql_client.is_missing_data_in_unit(): + raise Exception("Mysql data cannot be provided via config and relation") + + # Check relations + self._check_missing_dependencies(config) + + security_context_enabled = ( + config.security_context if not config.debug_mode else False + ) - # Registering custom internal events - self.framework.observe(self.on.configure_pod, self.configure_pod) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder( + enable_security_context=security_context_enabled + ) - # Registering required relation events - self.framework.observe( - self.on.kafka_relation_changed, self._on_kafka_relation_changed + # Add secrets to the pod + mongodb_secret_name = f"{self.app.name}-mongodb-secret" + pod_spec_builder.add_secret( + mongodb_secret_name, + {"uri": config.mongodb_uri or self.mongodb_client.connection_string}, ) - self.framework.observe( - self.on.mongodb_relation_changed, self._on_mongodb_relation_changed + mysql_secret_name = f"{self.app.name}-mysql-secret" + pod_spec_builder.add_secret( + mysql_secret_name, + { + "uri": config.mysql_uri + or self.mysql_client.get_root_uri(DEFAULT_MYSQL_DATABASE) + }, ) - # Registering required relation departed events - self.framework.observe( - self.on.kafka_relation_departed, self._on_kafka_relation_departed + # Build Container + container_builder = ContainerV3Builder( + self.app.name, + image_info, + config.image_pull_policy, + run_as_non_root=security_context_enabled, ) - self.framework.observe( - self.on.mongodb_relation_departed, self._on_mongodb_relation_departed + container_builder.add_port(name=self.app.name, port=PORT) + container_builder.add_envs( + { + # General configuration + "ALLOW_ANONYMOUS_LOGIN": "yes", + "OSMPOL_GLOBAL_LOGLEVEL": config.log_level, + # Kafka configuration + "OSMPOL_MESSAGE_DRIVER": "kafka", + "OSMPOL_MESSAGE_HOST": self.kafka.host, + "OSMPOL_MESSAGE_PORT": self.kafka.port, + # Database configuration + "OSMPOL_DATABASE_DRIVER": "mongo", + } ) + container_builder.add_secret_envs( + mongodb_secret_name, {"OSMPOL_DATABASE_URI": "uri"} + ) + container_builder.add_secret_envs( + mysql_secret_name, {"OSMPOL_SQL_DATABASE_URI": "uri"} + ) + container = container_builder.build() - def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn: - """Reads information about the kafka relation. - - Args: - event (EventBase): Kafka relation event. - """ - data_loc = event.unit if event.unit else event.app - - message_host = event.relation.data[data_loc].get("host") - message_port = event.relation.data[data_loc].get("port") - - if ( - message_host - and message_port - and ( - self.state.message_host != message_host - or self.state.message_port != message_port - ) - ): - self.state.message_host = message_host - self.state.message_port = message_port - self.on.configure_pod.emit() - - def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn: - """Clear kafka relation data. - - Args: - event (EventBase): Kafka relation event. - """ - self.state.message_host = None - self.state.message_port = None - self.on.configure_pod.emit() - - def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn: - """Reads information about the DB relation. - - Args: - event (EventBase): DB relation event. - """ - data_loc = event.unit if event.unit else event.app - - database_uri = event.relation.data[data_loc].get("connection_string") - - if database_uri and self.state.database_uri != database_uri: - self.state.database_uri = database_uri - self.on.configure_pod.emit() - - def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn: - """Clear mongodb relation data. - - Args: - event (EventBase): DB relation event. - """ - self.state.database_uri = None - self.on.configure_pod.emit() - - def _missing_relations(self) -> str: - """Checks if there missing relations. - - Returns: - str: string with missing relations - """ - data_status = { - "kafka": self.state.message_host, - "mongodb": self.state.database_uri, - } - - missing_relations = [k for k, v in data_status.items() if not v] - - return ", ".join(missing_relations) - - @property - def relation_state(self) -> Dict[str, Any]: - """Collects relation state configuration for pod spec assembly. - - Returns: - Dict[str, Any]: relation state information. - """ - relation_state = { - "message_host": self.state.message_host, - "message_port": self.state.message_port, - "database_uri": self.state.database_uri, - } - - return relation_state - - def configure_pod(self, event: EventBase) -> NoReturn: - """Assemble the pod spec and apply it, if possible. - - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if missing := self._missing_relations(): - self.unit.status = BlockedStatus( - "Waiting for {0} relation{1}".format( - missing, "s" if "," in missing else "" - ) - ) - return - - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - return - - self.unit.status = MaintenanceStatus("Assembling pod spec") - - # Fetch image information - try: - self.unit.status = MaintenanceStatus("Fetching image information") - image_info = self.image.fetch() - except OCIImageResourceError: - self.unit.status = BlockedStatus("Error fetching image information") - return - - try: - pod_spec = make_pod_spec( - image_info, - self.model.config, - self.relation_state, - self.model.app.name, - ) - except ValidationError as exc: - logger.exception("Config/Relation data validation error") - self.unit.status = BlockedStatus(str(exc)) - return - - if self.state.pod_spec != pod_spec: - self.model.pod.set_spec(pod_spec) - self.state.pod_spec = pod_spec - - self.unit.status = ActiveStatus("ready") + # Add Pod restart policy + restart_policy = PodRestartPolicy() + restart_policy.add_secrets( + secret_names=(mongodb_secret_name, mysql_secret_name) + ) + pod_spec_builder.set_restart_policy(restart_policy) + + # Add container to pod spec + pod_spec_builder.add_container(container) + + return pod_spec_builder.build() + + +VSCODE_WORKSPACE = { + "folders": [ + {"path": "/usr/lib/python3/dist-packages/osm_policy_module"}, + {"path": "/usr/lib/python3/dist-packages/osm_common"}, + ], + "settings": {}, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "POL", + "type": "python", + "request": "launch", + "module": "osm_policy_module.cmd.policy_module_agent", + "justMyCode": False, + } + ], + }, +} if __name__ == "__main__":