Coverage for src/charm.py : 98%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2# Copyright 2021 Canonical Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15#
16# For those usages not covered by the Apache License, Version 2.0 please
17# contact: legal@canonical.com
18#
19# To get in touch with the maintainers, please contact:
20# osm-charmers@lists.launchpad.net
21##
23# pylint: disable=E0213
26import logging
27from typing import NoReturn, Optional
30from ops.main import main
31from opslib.osm.charm import CharmedOsmBase, RelationsMissing
32from opslib.osm.interfaces.http import HttpClient
33from opslib.osm.interfaces.kafka import KafkaClient
34from opslib.osm.interfaces.mongo import MongoClient
35from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder
36from opslib.osm.validator import ModelValidator, validator
39logger = logging.getLogger(__name__)
41PORT = 9999
44class ConfigModel(ModelValidator):
45 vca_host: str
46 vca_port: int
47 vca_user: str
48 vca_secret: str
49 vca_pubkey: str
50 vca_cacert: str
51 vca_cloud: str
52 vca_k8s_cloud: str
53 database_commonkey: str
54 log_level: str
55 vca_apiproxy: Optional[str]
56 # Model-config options
57 vca_model_config_agent_metadata_url: Optional[str]
58 vca_model_config_agent_stream: Optional[str]
59 vca_model_config_apt_ftp_proxy: Optional[str]
60 vca_model_config_apt_http_proxy: Optional[str]
61 vca_model_config_apt_https_proxy: Optional[str]
62 vca_model_config_apt_mirror: Optional[str]
63 vca_model_config_apt_no_proxy: Optional[str]
64 vca_model_config_automatically_retry_hooks: Optional[bool]
65 vca_model_config_backup_dir: Optional[str]
66 vca_model_config_cloudinit_userdata: Optional[str]
67 vca_model_config_container_image_metadata_url: Optional[str]
68 vca_model_config_container_image_stream: Optional[str]
69 vca_model_config_container_inherit_properties: Optional[str]
70 vca_model_config_container_networking_method: Optional[str]
71 vca_model_config_default_series: Optional[str]
72 vca_model_config_default_space: Optional[str]
73 vca_model_config_development: Optional[bool]
74 vca_model_config_disable_network_management: Optional[bool]
75 vca_model_config_egress_subnets: Optional[str]
76 vca_model_config_enable_os_refresh_update: Optional[bool]
77 vca_model_config_enable_os_upgrade: Optional[bool]
78 vca_model_config_fan_config: Optional[str]
79 vca_model_config_firewall_mode: Optional[str]
80 vca_model_config_ftp_proxy: Optional[str]
81 vca_model_config_http_proxy: Optional[str]
82 vca_model_config_https_proxy: Optional[str]
83 vca_model_config_ignore_machine_addresses: Optional[bool]
84 vca_model_config_image_metadata_url: Optional[str]
85 vca_model_config_image_stream: Optional[str]
86 vca_model_config_juju_ftp_proxy: Optional[str]
87 vca_model_config_juju_http_proxy: Optional[str]
88 vca_model_config_juju_https_proxy: Optional[str]
89 vca_model_config_juju_no_proxy: Optional[str]
90 vca_model_config_logforward_enabled: Optional[bool]
91 vca_model_config_logging_config: Optional[str]
92 vca_model_config_lxd_snap_channel: Optional[str]
93 vca_model_config_max_action_results_age: Optional[str]
94 vca_model_config_max_action_results_size: Optional[str]
95 vca_model_config_max_status_history_age: Optional[str]
96 vca_model_config_max_status_history_size: Optional[str]
97 vca_model_config_net_bond_reconfigure_delay: Optional[str]
98 vca_model_config_no_proxy: Optional[str]
99 vca_model_config_provisioner_harvest_mode: Optional[str]
100 vca_model_config_proxy_ssh: Optional[bool]
101 vca_model_config_snap_http_proxy: Optional[str]
102 vca_model_config_snap_https_proxy: Optional[str]
103 vca_model_config_snap_store_assertions: Optional[str]
104 vca_model_config_snap_store_proxy: Optional[str]
105 vca_model_config_snap_store_proxy_url: Optional[str]
106 vca_model_config_ssl_hostname_verification: Optional[bool]
107 vca_model_config_test_mode: Optional[bool]
108 vca_model_config_transmit_vendor_metrics: Optional[bool]
109 vca_model_config_update_status_hook_interval: Optional[str]
111 @validator("log_level")
112 def validate_log_level(cls, v):
113 if v not in {"INFO", "DEBUG"}:
114 raise ValueError("value must be INFO or DEBUG")
115 return v
118class LcmCharm(CharmedOsmBase):
119 def __init__(self, *args) -> NoReturn:
120 super().__init__(*args, oci_image="image")
122 self.kafka_client = KafkaClient(self, "kafka")
123 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
124 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
126 self.mongodb_client = MongoClient(self, "mongodb")
127 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
128 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
130 self.ro_client = HttpClient(self, "ro")
131 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
132 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
134 def _check_missing_dependencies(self, config: ConfigModel):
135 missing_relations = []
137 if self.kafka_client.is_missing_data_in_unit():
138 missing_relations.append("kafka")
139 if self.mongodb_client.is_missing_data_in_unit():
140 missing_relations.append("mongodb")
141 if self.ro_client.is_missing_data_in_app():
142 missing_relations.append("ro")
144 if missing_relations:
145 raise RelationsMissing(missing_relations)
147 def build_pod_spec(self, image_info):
148 # Validate config
149 config = ConfigModel(**dict(self.config))
150 # Check relations
151 self._check_missing_dependencies(config)
152 # Create Builder for the PodSpec
153 pod_spec_builder = PodSpecV3Builder()
154 # Build Container
155 container_builder = ContainerV3Builder(self.app.name, image_info)
156 container_builder.add_port(name=self.app.name, port=PORT)
157 container_builder.add_envs(
158 {
159 # General configuration
160 "ALLOW_ANONYMOUS_LOGIN": "yes",
161 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
162 # RO configuration
163 "OSMLCM_RO_HOST": self.ro_client.host,
164 "OSMLCM_RO_PORT": self.ro_client.port,
165 "OSMLCM_RO_TENANT": "osm",
166 # Kafka configuration
167 "OSMLCM_MESSAGE_DRIVER": "kafka",
168 "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
169 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
170 # Database configuration
171 "OSMLCM_DATABASE_DRIVER": "mongo",
172 "OSMLCM_DATABASE_URI": self.mongodb_client.connection_string,
173 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
174 # Storage configuration
175 "OSMLCM_STORAGE_DRIVER": "mongo",
176 "OSMLCM_STORAGE_PATH": "/app/storage",
177 "OSMLCM_STORAGE_COLLECTION": "files",
178 "OSMLCM_STORAGE_URI": self.mongodb_client.connection_string,
179 # VCA configuration
180 "OSMLCM_VCA_HOST": config.vca_host,
181 "OSMLCM_VCA_PORT": config.vca_port,
182 "OSMLCM_VCA_USER": config.vca_user,
183 "OSMLCM_VCA_PUBKEY": config.vca_pubkey,
184 "OSMLCM_VCA_SECRET": config.vca_secret,
185 "OSMLCM_VCA_CACERT": config.vca_cacert,
186 "OSMLCM_VCA_CLOUD": config.vca_cloud,
187 "OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud,
188 }
189 )
190 if config.vca_apiproxy:
191 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
193 model_config_envs = {
194 f"OSMLCM_{k.upper()}": v
195 for k, v in self.config.items()
196 if k.startswith("vca_model_config")
197 }
198 if model_config_envs:
199 container_builder.add_envs(model_config_envs)
200 container = container_builder.build()
201 # Add container to pod spec
202 pod_spec_builder.add_container(container)
203 return pod_spec_builder.build()
206if __name__ == "__main__":
207 main(LcmCharm)
210# class ConfigurePodEvent(EventBase):
211# """Configure Pod event"""
213# pass
216# class LcmEvents(CharmEvents):
217# """LCM Events"""
219# configure_pod = EventSource(ConfigurePodEvent)
222# class LcmCharm(CharmBase):
223# """LCM Charm."""
225# state = StoredState()
226# on = LcmEvents()
228# def __init__(self, *args) -> NoReturn:
229# """LCM Charm constructor."""
230# super().__init__(*args)
232# # Internal state initialization
233# self.state.set_default(pod_spec=None)
235# # Message bus data initialization
236# self.state.set_default(message_host=None)
237# self.state.set_default(message_port=None)
239# # Database data initialization
240# self.state.set_default(database_uri=None)
242# # RO data initialization
243# self.state.set_default(ro_host=None)
244# self.state.set_default(ro_port=None)
246# self.port = LCM_PORT
247# self.image = OCIImageResource(self, "image")
249# # Registering regular events
250# self.framework.observe(self.on.start, self.configure_pod)
251# self.framework.observe(self.on.config_changed, self.configure_pod)
252# self.framework.observe(self.on.upgrade_charm, self.configure_pod)
254# # Registering custom internal events
255# self.framework.observe(self.on.configure_pod, self.configure_pod)
257# # Registering required relation events
258# self.framework.observe(
259# self.on.kafka_relation_changed, self._on_kafka_relation_changed
260# )
261# self.framework.observe(
262# self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
263# )
264# self.framework.observe(
265# self.on.ro_relation_changed, self._on_ro_relation_changed
266# )
268# # Registering required relation broken events
269# self.framework.observe(
270# self.on.kafka_relation_broken, self._on_kafka_relation_broken
271# )
272# self.framework.observe(
273# self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
274# )
275# self.framework.observe(
276# self.on.ro_relation_broken, self._on_ro_relation_broken
277# )
279# def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
280# """Reads information about the kafka relation.
282# Args:
283# event (EventBase): Kafka relation event.
284# """
285# message_host = event.relation.data[event.unit].get("host")
286# message_port = event.relation.data[event.unit].get("port")
288# if (
289# message_host
290# and message_port
291# and (
292# self.state.message_host != message_host
293# or self.state.message_port != message_port
294# )
295# ):
296# self.state.message_host = message_host
297# self.state.message_port = message_port
298# self.on.configure_pod.emit()
300# def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
301# """Clears data from kafka relation.
303# Args:
304# event (EventBase): Kafka relation event.
305# """
306# self.state.message_host = None
307# self.state.message_port = None
308# self.on.configure_pod.emit()
310# def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
311# """Reads information about the DB relation.
313# Args:
314# event (EventBase): DB relation event.
315# """
316# database_uri = event.relation.data[event.unit].get("connection_string")
318# if database_uri and self.state.database_uri != database_uri:
319# self.state.database_uri = database_uri
320# self.on.configure_pod.emit()
322# def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
323# """Clears data from mongodb relation.
325# Args:
326# event (EventBase): DB relation event.
327# """
328# self.state.database_uri = None
329# self.on.configure_pod.emit()
331# def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
332# """Reads information about the RO relation.
334# Args:
335# event (EventBase): Keystone relation event.
336# """
337# ro_host = event.relation.data[event.unit].get("host")
338# ro_port = event.relation.data[event.unit].get("port")
340# if (
341# ro_host
342# and ro_port
343# and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
344# ):
345# self.state.ro_host = ro_host
346# self.state.ro_port = ro_port
347# self.on.configure_pod.emit()
349# def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
350# """Clears data from ro relation.
352# Args:
353# event (EventBase): Keystone relation event.
354# """
355# self.state.ro_host = None
356# self.state.ro_port = None
357# self.on.configure_pod.emit()
359# def _missing_relations(self) -> str:
360# """Checks if there missing relations.
362# Returns:
363# str: string with missing relations
364# """
365# data_status = {
366# "kafka": self.state.message_host,
367# "mongodb": self.state.database_uri,
368# "ro": self.state.ro_host,
369# }
371# missing_relations = [k for k, v in data_status.items() if not v]
373# return ", ".join(missing_relations)
375# @property
376# def relation_state(self) -> Dict[str, Any]:
377# """Collects relation state configuration for pod spec assembly.
379# Returns:
380# Dict[str, Any]: relation state information.
381# """
382# relation_state = {
383# "message_host": self.state.message_host,
384# "message_port": self.state.message_port,
385# "database_uri": self.state.database_uri,
386# "ro_host": self.state.ro_host,
387# "ro_port": self.state.ro_port,
388# }
390# return relation_state
392# def configure_pod(self, event: EventBase) -> NoReturn:
393# """Assemble the pod spec and apply it, if possible.
395# Args:
396# event (EventBase): Hook or Relation event that started the
397# function.
398# """
399# if missing := self._missing_relations():
400# self.unit.status = BlockedStatus(
401# "Waiting for {0} relation{1}".format(
402# missing, "s" if "," in missing else ""
403# )
404# )
405# return
407# if not self.unit.is_leader():
408# self.unit.status = ActiveStatus("ready")
409# return
411# self.unit.status = MaintenanceStatus("Assembling pod spec")
413# # Fetch image information
414# try:
415# self.unit.status = MaintenanceStatus("Fetching image information")
416# image_info = self.image.fetch()
417# except OCIImageResourceError:
418# self.unit.status = BlockedStatus("Error fetching image information")
419# return
421# try:
422# pod_spec = make_pod_spec(
423# image_info,
424# self.model.config,
425# self.relation_state,
426# self.model.app.name,
427# self.port,
428# )
429# except ValueError as exc:
430# logger.exception("Config/Relation data validation error")
431# self.unit.status = BlockedStatus(str(exc))
432# return
434# if self.state.pod_spec != pod_spec:
435# self.model.pod.set_spec(pod_spec)
436# self.state.pod_spec = pod_spec
438# self.unit.status = ActiveStatus("ready")
441# if __name__ == "__main__":
442# main(LcmCharm)