| #!/usr/bin/env python3 |
| # Copyright 2020 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 |
| # |
| # 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. |
| # |
| # 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 logging |
| from typing import Any, Dict, NoReturn |
| |
| from ops.charm import CharmBase, CharmEvents |
| from ops.framework import EventBase, EventSource, StoredState |
| from ops.main import main |
| from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus |
| from oci_image import OCIImageResource, OCIImageResourceError |
| |
| from pod_spec import make_pod_spec |
| |
| logger = logging.getLogger(__name__) |
| |
| NBI_PORT = 9999 |
| |
| |
| class ConfigurePodEvent(EventBase): |
| """Configure Pod event""" |
| |
| pass |
| |
| |
| class NbiEvents(CharmEvents): |
| """NBI Events""" |
| |
| configure_pod = EventSource(ConfigurePodEvent) |
| |
| |
| class NbiCharm(CharmBase): |
| """NBI Charm.""" |
| |
| state = StoredState() |
| on = NbiEvents() |
| |
| def __init__(self, *args) -> NoReturn: |
| """NBI 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) |
| |
| # Prometheus data initialization |
| self.state.set_default(prometheus_host=None) |
| self.state.set_default(prometheus_port=None) |
| |
| # Keystone data initialization |
| self.state.set_default(keystone_host=None) |
| self.state.set_default(keystone_port=None) |
| self.state.set_default(keystone_user_domain_name=None) |
| self.state.set_default(keystone_project_domain_name=None) |
| self.state.set_default(keystone_username=None) |
| self.state.set_default(keystone_password=None) |
| self.state.set_default(keystone_service=None) |
| |
| self.port = NBI_PORT |
| 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) |
| |
| # Registering custom internal events |
| self.framework.observe(self.on.configure_pod, self.configure_pod) |
| |
| # Registering required relation changed events |
| self.framework.observe( |
| self.on.kafka_relation_changed, self._on_kafka_relation_changed |
| ) |
| self.framework.observe( |
| self.on.mongodb_relation_changed, self._on_mongodb_relation_changed |
| ) |
| self.framework.observe( |
| self.on.keystone_relation_changed, self._on_keystone_relation_changed |
| ) |
| self.framework.observe( |
| self.on.prometheus_relation_changed, self._on_prometheus_relation_changed |
| ) |
| |
| # Registering required relation departed events |
| self.framework.observe( |
| self.on.kafka_relation_departed, self._on_kafka_relation_departed |
| ) |
| self.framework.observe( |
| self.on.mongodb_relation_departed, self._on_mongodb_relation_departed |
| ) |
| self.framework.observe( |
| self.on.keystone_relation_departed, self._on_keystone_relation_departed |
| ) |
| self.framework.observe( |
| self.on.prometheus_relation_departed, self._on_prometheus_relation_departed |
| ) |
| |
| # Registering provided relation events |
| self.framework.observe(self.on.nbi_relation_joined, self._publish_nbi_info) |
| |
| 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 = int(message_port) |
| self.on.configure_pod.emit() |
| |
| def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn: |
| """Clears data from kafka relation. |
| |
| 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: |
| """Clears data from mongodb relation. |
| |
| Args: |
| event (EventBase): DB relation event. |
| """ |
| self.state.database_uri = None |
| self.on.configure_pod.emit() |
| |
| def _on_keystone_relation_changed(self, event: EventBase) -> NoReturn: |
| """Reads information about the keystone relation. |
| |
| Args: |
| event (EventBase): Keystone relation event. |
| """ |
| data_loc = event.unit if event.unit else event.app |
| |
| keystone_host = event.relation.data[data_loc].get("host") |
| keystone_port = event.relation.data[data_loc].get("port") |
| keystone_user_domain_name = event.relation.data[data_loc].get( |
| "user_domain_name" |
| ) |
| keystone_project_domain_name = event.relation.data[data_loc].get( |
| "project_domain_name" |
| ) |
| keystone_username = event.relation.data[data_loc].get("username") |
| keystone_password = event.relation.data[data_loc].get("password") |
| keystone_service = event.relation.data[data_loc].get("service") |
| |
| if ( |
| keystone_host |
| and keystone_port |
| and keystone_user_domain_name |
| and keystone_project_domain_name |
| and keystone_username |
| and keystone_password |
| and keystone_service |
| and ( |
| self.state.keystone_host != keystone_host |
| or self.state.keystone_port != keystone_port |
| or self.state.keystone_user_domain_name != keystone_user_domain_name |
| or self.state.keystone_project_domain_name |
| != keystone_project_domain_name |
| or self.state.keystone_username != keystone_username |
| or self.state.keystone_password != keystone_password |
| or self.state.keystone_service != keystone_service |
| ) |
| ): |
| self.state.keystone_host = keystone_host |
| self.state.keystone_port = int(keystone_port) |
| self.state.keystone_user_domain_name = keystone_user_domain_name |
| self.state.keystone_project_domain_name = keystone_project_domain_name |
| self.state.keystone_username = keystone_username |
| self.state.keystone_password = keystone_password |
| self.state.keystone_service = keystone_service |
| self.on.configure_pod.emit() |
| |
| def _on_keystone_relation_departed(self, event: EventBase) -> NoReturn: |
| """Clears data from keystone relation. |
| |
| Args: |
| event (EventBase): Keystone relation event. |
| """ |
| self.state.keystone_host = None |
| self.state.keystone_port = None |
| self.state.keystone_user_domain_name = None |
| self.state.keystone_project_domain_name = None |
| self.state.keystone_username = None |
| self.state.keystone_password = None |
| self.state.keystone_service = None |
| self.on.configure_pod.emit() |
| |
| def _on_prometheus_relation_changed(self, event: EventBase) -> NoReturn: |
| """Reads information about the prometheus relation. |
| |
| Args: |
| event (EventBase): Prometheus relation event. |
| """ |
| data_loc = event.unit if event.unit else event.app |
| |
| prometheus_host = event.relation.data[data_loc].get("hostname") |
| prometheus_port = event.relation.data[data_loc].get("port") |
| |
| if ( |
| prometheus_host |
| and prometheus_port |
| and ( |
| self.state.prometheus_host != prometheus_host |
| or self.state.prometheus_port != prometheus_port |
| ) |
| ): |
| self.state.prometheus_host = prometheus_host |
| self.state.prometheus_port = int(prometheus_port) |
| self.on.configure_pod.emit() |
| |
| def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn: |
| """Clears data from prometheus relation. |
| |
| Args: |
| event (EventBase): Prometheus relation event. |
| """ |
| self.state.prometheus_host = None |
| self.state.prometheus_port = None |
| self.on.configure_pod.emit() |
| |
| def _publish_nbi_info(self, event: EventBase) -> NoReturn: |
| """Publishes NBI information. |
| |
| Args: |
| event (EventBase): NBI relation event. |
| """ |
| rel_data = { |
| "host": self.model.app.name, |
| "port": str(NBI_PORT), |
| } |
| for k, v in rel_data.items(): |
| event.relation.data[self.unit][k] = v |
| |
| 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, |
| "prometheus": self.state.prometheus_host, |
| } |
| |
| if self.model.config["auth_backend"] == "keystone": |
| data_status["keystone"] = self.state.keystone_host |
| |
| 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, |
| "prometheus_host": self.state.prometheus_host, |
| "prometheus_port": self.state.prometheus_port, |
| } |
| |
| if self.model.config["auth_backend"] == "keystone": |
| relation_state.update( |
| { |
| "keystone_host": self.state.keystone_host, |
| "keystone_port": self.state.keystone_port, |
| "keystone_user_domain_name": self.state.keystone_user_domain_name, |
| "keystone_project_domain_name": self.state.keystone_project_domain_name, |
| "keystone_username": self.state.keystone_username, |
| "keystone_password": self.state.keystone_password, |
| "keystone_service": self.state.keystone_service, |
| } |
| ) |
| |
| 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( |
| f"Waiting for {missing} relation{'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, |
| self.port, |
| ) |
| 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") |
| |
| |
| if __name__ == "__main__": |
| main(NbiCharm) |