blob: 270a5479a837a791859d2f2143eb5e71c5afb030 [file] [log] [blame]
#!/usr/bin/env python3
# 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
#
# 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
##
# pylint: disable=E0213
import logging
from typing import Optional, NoReturn
from ops.main import main
from opslib.osm.charm import CharmedOsmBase, RelationsMissing
from opslib.osm.pod import (
ContainerV3Builder,
PodSpecV3Builder,
)
from opslib.osm.validator import (
ModelValidator,
validator,
)
from opslib.osm.interfaces.kafka import KafkaClient
from opslib.osm.interfaces.mongo import MongoClient
from opslib.osm.interfaces.http import HttpClient
logger = logging.getLogger(__name__)
PORT = 9999
class ConfigModel(ModelValidator):
vca_host: str
vca_port: int
vca_user: str
vca_password: str
vca_pubkey: str
vca_cacert: str
vca_cloud: str
vca_k8s_cloud: str
database_commonkey: str
log_level: str
vca_apiproxy: 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
class LcmCharm(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)
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.ro_client = HttpClient(self, "ro")
self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
def _check_missing_dependencies(self, config: ConfigModel):
missing_relations = []
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")
if self.ro_client.is_missing_data_in_app():
missing_relations.append("ro")
if missing_relations:
raise RelationsMissing(missing_relations)
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)
container_builder.add_port(name=self.app.name, port=PORT)
container_builder.add_envs(
{
# General configuration
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
# RO configuration
"OSMLCM_RO_HOST": self.ro_client.host,
"OSMLCM_RO_PORT": self.ro_client.port,
"OSMLCM_RO_TENANT": "osm",
# Kafka configuration
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": self.kafka_client.host,
"OSMLCM_MESSAGE_PORT": self.kafka_client.port,
# Database configuration
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": self.mongodb_client.connection_string,
"OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
# Storage configuration
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": self.mongodb_client.connection_string,
# VCA configuration
"OSMLCM_VCA_HOST": config.vca_host,
"OSMLCM_VCA_PORT": config.vca_port,
"OSMLCM_VCA_USER": config.vca_user,
"OSMLCM_VCA_PUBKEY": config.vca_pubkey,
"OSMLCM_VCA_SECRET": config.vca_password,
"OSMLCM_VCA_CACERT": config.vca_cacert,
"OSMLCM_VCA_CLOUD": config.vca_cloud,
"OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud,
}
)
if config.vca_apiproxy:
container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
container = container_builder.build()
# Add container to pod spec
pod_spec_builder.add_container(container)
return pod_spec_builder.build()
if __name__ == "__main__":
main(LcmCharm)
# class ConfigurePodEvent(EventBase):
# """Configure Pod event"""
# pass
# class LcmEvents(CharmEvents):
# """LCM Events"""
# configure_pod = EventSource(ConfigurePodEvent)
# class LcmCharm(CharmBase):
# """LCM Charm."""
# state = StoredState()
# on = LcmEvents()
# def __init__(self, *args) -> NoReturn:
# """LCM 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)
# # RO data initialization
# self.state.set_default(ro_host=None)
# self.state.set_default(ro_port=None)
# self.port = LCM_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 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.ro_relation_changed, self._on_ro_relation_changed
# )
# # Registering required relation broken events
# self.framework.observe(
# self.on.kafka_relation_broken, self._on_kafka_relation_broken
# )
# self.framework.observe(
# self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
# )
# self.framework.observe(
# self.on.ro_relation_broken, self._on_ro_relation_broken
# )
# def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
# """Reads information about the kafka relation.
# Args:
# event (EventBase): Kafka relation event.
# """
# message_host = event.relation.data[event.unit].get("host")
# message_port = event.relation.data[event.unit].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_broken(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.
# """
# database_uri = event.relation.data[event.unit].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_broken(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_ro_relation_changed(self, event: EventBase) -> NoReturn:
# """Reads information about the RO relation.
# Args:
# event (EventBase): Keystone relation event.
# """
# ro_host = event.relation.data[event.unit].get("host")
# ro_port = event.relation.data[event.unit].get("port")
# if (
# ro_host
# and ro_port
# and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
# ):
# self.state.ro_host = ro_host
# self.state.ro_port = ro_port
# self.on.configure_pod.emit()
# def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
# """Clears data from ro relation.
# Args:
# event (EventBase): Keystone relation event.
# """
# self.state.ro_host = None
# self.state.ro_port = 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,
# "ro": self.state.ro_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,
# "ro_host": self.state.ro_host,
# "ro_port": self.state.ro_port,
# }
# 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,
# 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(LcmCharm)