X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fro%2Fsrc%2Fcharm.py;h=5b40c1606f2e6197002cb6554f84a890591563d4;hb=5d1ec6e86da83820d316bb52d6586f9dc27106de;hp=8e6d5764e40175c4ffb3e617cc39ff4117bc6a70;hpb=ccfacbbb3d3bd025f48f2a1434e0b6bdeae64ead;p=osm%2Fdevops.git diff --git a/installers/charm/ro/src/charm.py b/installers/charm/ro/src/charm.py index 8e6d5764..5b40c160 100755 --- a/installers/charm/ro/src/charm.py +++ b/installers/charm/ro/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,116 +20,93 @@ # osm-charmers@lists.launchpad.net ## +# pylint: disable=E0213 + +import base64 import logging -from typing import Dict, List, NoReturn +from typing import NoReturn, Optional -from ops.charm import CharmBase -from ops.framework import EventBase, StoredState from ops.main import main -from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit -from oci_image import OCIImageResource, OCIImageResourceError - -from pod_spec import make_pod_spec +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.interfaces.kafka import KafkaClient +from opslib.osm.interfaces.mongo import MongoClient +from opslib.osm.interfaces.mysql import MysqlClient +from opslib.osm.pod import ContainerV3Builder, FilesV3Builder, PodSpecV3Builder +from opslib.osm.validator import ModelValidator, validator logger = logging.getLogger(__name__) -RO_PORT = 9090 +PORT = 9090 -class RelationsMissing(Exception): - def __init__(self, missing_relations: List): - self.message = "" - if missing_relations and isinstance(missing_relations, list): - self.message += f'Waiting for {", ".join(missing_relations)} relation' - if "," in self.message: - self.message += "s" +def _check_certificate_data(name: str, content: str): + if not name or not content: + raise ValueError("certificate name and content must be a non-empty string") -class RelationDefinition: - def __init__(self, relation_name: str, keys: List, source_type): - if source_type != Application and source_type != Unit: - raise TypeError( - "source_type should be ops.model.Application or ops.model.Unit" - ) - self.relation_name = relation_name - self.keys = keys - self.source_type = source_type - - -def check_missing_relation_data( - data: Dict, - expected_relations_data: List[RelationDefinition], -): - missing_relations = [] - for relation_data in expected_relations_data: - if not all( - f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys - ): - missing_relations.append(relation_data.relation_name) - if missing_relations: - raise RelationsMissing(missing_relations) - - -def get_relation_data( - charm: CharmBase, - relation_data: RelationDefinition, -) -> Dict: - data = {} - relation = charm.model.get_relation(relation_data.relation_name) - if relation: - self_app_unit = ( - charm.app if relation_data.source_type == Application else charm.unit - ) - expected_type = relation_data.source_type - for app_unit in relation.data: - if app_unit != self_app_unit and isinstance(app_unit, expected_type): - if all(k in relation.data[app_unit] for k in relation_data.keys): - for k in relation_data.keys: - data[f"{relation_data.relation_name}_{k}"] = relation.data[ - app_unit - ].get(k) - break - return data +def _extract_certificates(certs_config: str): + certificates = {} + if certs_config: + cert_list = certs_config.split(",") + for cert in cert_list: + name, content = cert.split(":") + _check_certificate_data(name, content) + certificates[name] = content + return certificates -class RoCharm(CharmBase): - """RO Charm.""" +def decode(content: str): + return base64.b64decode(content.encode("utf-8")).decode("utf-8") - state = StoredState() - def __init__(self, *args) -> NoReturn: - """RO Charm constructor.""" - super().__init__(*args) +class ConfigModel(ModelValidator): + enable_ng_ro: bool + database_commonkey: str + log_level: str + vim_database: str + ro_database: str + openmano_tenant: str + certificates: Optional[str] - # Internal state initialization - self.state.set_default(pod_spec=None) + @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 - self.port = RO_PORT - self.image = OCIImageResource(self, "image") + @validator("certificates") + def validate_certificates(cls, v): + # Raises an exception if it cannot extract the certificates + _extract_certificates(v) + return v - # Registering regular events - self.framework.observe(self.on.start, self.configure_pod) - self.framework.observe(self.on.config_changed, self.configure_pod) + @property + def certificates_dict(cls): + return _extract_certificates(cls.certificates) if cls.certificates else {} - # Registering required relation events - self.framework.observe(self.on.kafka_relation_changed, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod) - self.framework.observe(self.on.mysql_relation_changed, self.configure_pod) - # Registering required relation departed events - self.framework.observe(self.on.kafka_relation_departed, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod) - self.framework.observe(self.on.mysql_relation_departed, self.configure_pod) +class RoCharm(CharmedOsmBase): + """GrafanaCharm Charm.""" - # Registering required relation broken events - self.framework.observe(self.on.kafka_relation_broken, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_broken, self.configure_pod) - self.framework.observe(self.on.mysql_relation_broken, self.configure_pod) + def __init__(self, *args) -> NoReturn: + """Prometheus Charm constructor.""" + super().__init__(*args, oci_image="image") + + self.kafka_client = KafkaClient(self, "kafka") + self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod) + self.framework.observe(self.on["kafka"].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) - # Registering provided relation events - self.framework.observe(self.on.ro_relation_joined, self._publish_ro_info) + 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) - def _publish_ro_info(self, event: EventBase) -> NoReturn: + self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info) + + def _publish_ro_info(self, event): """Publishes RO information. Args: @@ -138,80 +115,103 @@ class RoCharm(CharmBase): if self.unit.is_leader(): rel_data = { "host": self.model.app.name, - "port": str(RO_PORT), + "port": str(PORT), } for k, v in rel_data.items(): event.relation.data[self.app][k] = v - @property - def relations_requirements(self): - if self.model.config["enable_ng_ro"]: - return [ - RelationDefinition("kafka", ["host", "port"], Unit), - RelationDefinition("mongodb", ["connection_string"], Unit), - ] - else: - return [ - RelationDefinition( - "mysql", ["host", "port", "user", "password", "root_password"], Unit - ) - ] - - def get_relation_state(self): - relation_state = {} - for relation_requirements in self.relations_requirements: - data = get_relation_data(self, relation_requirements) - relation_state = {**relation_state, **data} - check_missing_relation_data(relation_state, self.relations_requirements) - return relation_state - - def configure_pod(self, _=None) -> NoReturn: - """Assemble the pod spec and apply it, if possible. + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - return - - relation_state = None - try: - relation_state = self.get_relation_state() - except RelationsMissing as exc: - logger.exception("Relation missing error") - self.unit.status = BlockedStatus(exc.message) - 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, - relation_state, - self.model.app.name, - self.port, + if config.enable_ng_ro: + if self.kafka_client.is_missing_data_in_unit(): + missing_relations.append("kafka") + if self.mongodb_client.is_missing_data_in_unit(): + missing_relations.append("mongodb") + else: + if self.mysql_client.is_missing_data_in_unit(): + missing_relations.append("mysql") + if missing_relations: + raise RelationsMissing(missing_relations) + + def _build_cert_files( + self, + config: ConfigModel, + ): + cert_files_builder = FilesV3Builder() + for name, content in config.certificates_dict.items(): + cert_files_builder.add_file(name, decode(content), mode=0o600) + return cert_files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + # Check relations + self._check_missing_dependencies(config) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + # Build Container + container_builder = ContainerV3Builder(self.app.name, image_info) + certs_files = self._build_cert_files(config) + if certs_files: + container_builder.add_volume_config("certs", "/certs", certs_files) + container_builder.add_port(name=self.app.name, port=PORT) + container_builder.add_http_readiness_probe( + "/ro/" if config.enable_ng_ro else "/openmano/tenants", + PORT, + initial_delay_seconds=10, + period_seconds=10, + timeout_seconds=5, + failure_threshold=3, + ) + container_builder.add_http_liveness_probe( + "/ro/" if config.enable_ng_ro else "/openmano/tenants", + PORT, + initial_delay_seconds=600, + period_seconds=10, + timeout_seconds=5, + failure_threshold=3, + ) + container_builder.add_envs( + { + "OSMRO_LOG_LEVEL": config.log_level, + } + ) + if config.enable_ng_ro: + container_builder.add_envs( + { + "OSMRO_MESSAGE_DRIVER": "kafka", + "OSMRO_MESSAGE_HOST": self.kafka_client.host, + "OSMRO_MESSAGE_PORT": self.kafka_client.port, + # MongoDB configuration + "OSMRO_DATABASE_DRIVER": "mongo", + "OSMRO_DATABASE_URI": self.mongodb_client.connection_string, + "OSMRO_DATABASE_COMMONKEY": config.database_commonkey, + } ) - except ValueError 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") + else: + container_builder.add_envs( + { + "RO_DB_HOST": self.mysql_client.host, + "RO_DB_OVIM_HOST": self.mysql_client.host, + "RO_DB_PORT": self.mysql_client.port, + "RO_DB_OVIM_PORT": self.mysql_client.port, + "RO_DB_USER": self.mysql_client.user, + "RO_DB_OVIM_USER": self.mysql_client.user, + "RO_DB_PASSWORD": self.mysql_client.password, + "RO_DB_OVIM_PASSWORD": self.mysql_client.password, + "RO_DB_ROOT_PASSWORD": self.mysql_client.root_password, + "RO_DB_OVIM_ROOT_PASSWORD": self.mysql_client.root_password, + "RO_DB_NAME": config.ro_database, + "RO_DB_OVIM_NAME": config.vim_database, + "OPENMANO_TENANT": config.openmano_tenant, + } + ) + container = container_builder.build() + # Add container to pod spec + pod_spec_builder.add_container(container) + return pod_spec_builder.build() if __name__ == "__main__":