X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fpla%2Fsrc%2Fcharm.py;h=2a08ea59ffaea6c9774d8c5220baa45db922f9b1;hb=3ddbbd1f6c70306d13db0976e1e6b3bda0c69abd;hp=1fc6386fa64270e98667e97fc0abf40115a917ed;hpb=a4a37f7f9f5410ff2c7833b76bdc85f752c74849;p=osm%2Fdevops.git diff --git a/installers/charm/pla/src/charm.py b/installers/charm/pla/src/charm.py index 1fc6386f..2a08ea59 100755 --- a/installers/charm/pla/src/charm.py +++ b/installers/charm/pla/src/charm.py @@ -1,147 +1,143 @@ #!/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 a copy of the License at +# 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 +# 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. +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## -import sys -import logging +# pylint: disable=E0213 -sys.path.append("lib") -from ops.charm import CharmBase -from ops.framework import StoredState, Object +import logging +from typing import NoReturn, Optional + from ops.main import main -from ops.model import ( - ActiveStatus, - MaintenanceStatus, - WaitingStatus, +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.interfaces.kafka import KafkaClient +from opslib.osm.interfaces.mongo import MongoClient +from opslib.osm.pod import ( + ContainerV3Builder, + PodSpecV3Builder, ) +from opslib.osm.validator import ModelValidator, validator -from glob import glob -from pathlib import Path -from string import Template logger = logging.getLogger(__name__) +PORT = 9999 + + +class ConfigModel(ModelValidator): + database_commonkey: str + mongodb_uri: Optional[str] + log_level: str + image_pull_policy: Optional[str] + + @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("mongodb_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("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 PlaCharm(CharmedOsmBase): + def __init__(self, *args) -> NoReturn: + 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) -class PLACharm(CharmBase): - state = StoredState() + 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 __init__(self, framework, key): - super().__init__(framework, key) - self.state.set_default(spec=None) - self.state.set_default(kafka_host=None) - self.state.set_default(kafka_port=None) - self.state.set_default(mongodb_uri=None) + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] - # Observe Charm related events - self.framework.observe(self.on.config_changed, self.on_config_changed) - self.framework.observe(self.on.start, self.on_start) - self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm) + if self.kafka_client.is_missing_data_in_unit(): + missing_relations.append("kafka") + if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit(): + missing_relations.append("mongodb") - # Relations - self.framework.observe( - self.on.kafka_relation_changed, self.on_kafka_relation_changed + 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") + + # 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, config.image_pull_policy ) - self.framework.observe( - self.on.mongo_relation_changed, self.on_mongo_relation_changed + container_builder.add_port(name=self.app.name, port=PORT) + container_builder.add_envs( + { + # General configuration + "ALLOW_ANONYMOUS_LOGIN": "yes", + "OSMPLA_GLOBAL_LOG_LEVEL": config.log_level, + # Kafka configuration + "OSMPLA_MESSAGE_DRIVER": "kafka", + "OSMPLA_MESSAGE_HOST": self.kafka_client.host, + "OSMPLA_MESSAGE_PORT": self.kafka_client.port, + # Database configuration + "OSMPLA_DATABASE_DRIVER": "mongo", + "OSMPLA_DATABASE_URI": config.mongodb_uri + or self.mongodb_client.connection_string, + "OSMPLA_DATABASE_COMMONKEY": config.database_commonkey, + } ) - def _apply_spec(self): - # Only apply the spec if this unit is a leader. - unit = self.model.unit - if not unit.is_leader(): - unit.status = ActiveStatus("ready") - return - if not self.state.kafka_host or not self.state.kafka_port: - unit.status = WaitingStatus("Waiting for Kafka") - return - if not self.state.mongodb_uri: - unit.status = WaitingStatus("Waiting for MongoDB") - return - - unit.status = MaintenanceStatus("Applying new pod spec") - - new_spec = self.make_pod_spec() - if new_spec == self.state.spec: - unit.status = ActiveStatus("ready") - return - self.framework.model.pod.set_spec(new_spec) - self.state.spec = new_spec - unit.status = ActiveStatus("ready") - - def make_pod_spec(self): - config = self.framework.model.config - - ports = [ - {"name": "port", "containerPort": config["port"], "protocol": "TCP", }, - ] - - config_spec = { - "OSMPLA_MESSAGE_DRIVER": "kafka", - "OSMPLA_MESSAGE_HOST": self.state.kafka_host, - "OSMPLA_MESSAGE_PORT": self.state.kafka_port, - "OSMPLA_DATABASE_DRIVER": "mongo", - "OSMPLA_DATABASE_URI": self.state.mongodb_uri, - "OSMPLA_GLOBAL_LOG_LEVEL": config["log_level"], - "OSMPLA_DATABASE_COMMONKEY": config["database_common_key"], - } + container = container_builder.build() - spec = { - "version": 2, - "containers": [ - { - "name": self.framework.model.app.name, - "image": config["image"], - "ports": ports, - "config": config_spec, - } - ], - } + # Add container to pod spec + pod_spec_builder.add_container(container) - return spec - - def on_config_changed(self, event): - """Handle changes in configuration""" - self._apply_spec() - - def on_start(self, event): - """Called when the charm is being installed""" - self._apply_spec() - - def on_upgrade_charm(self, event): - """Upgrade the charm.""" - unit = self.model.unit - unit.status = MaintenanceStatus("Upgrading charm") - self.on_start(event) - - def on_kafka_relation_changed(self, event): - unit = self.model.unit - if not unit.is_leader(): - return - self.state.kafka_host = event.relation.data[event.unit].get("host") - self.state.kafka_port = event.relation.data[event.unit].get("port") - self._apply_spec() - - def on_mongo_relation_changed(self, event): - unit = self.model.unit - if not unit.is_leader(): - return - self.state.mongodb_uri = event.relation.data[event.unit].get( - "connection_string" - ) - self._apply_spec() + return pod_spec_builder.build() if __name__ == "__main__": - main(PLACharm) + main(PlaCharm)