blob: f2b2ecf77da1be3a1b28273b03a50cb930e3b2ba [file] [log] [blame]
#!/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)