Major improvement in OSM charms
[osm/devops.git] / installers / charm / lcm / src / charm.py
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 ##
22
23 # pylint: disable=E0213
24
25
26 import logging
27 from typing import Optional, NoReturn
28
29 from ops.main import main
30
31 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
32
33 from opslib.osm.pod import (
34 ContainerV3Builder,
35 PodSpecV3Builder,
36 )
37
38 from opslib.osm.validator import (
39 ModelValidator,
40 validator,
41 )
42
43 from opslib.osm.interfaces.kafka import KafkaClient
44 from opslib.osm.interfaces.mongo import MongoClient
45 from opslib.osm.interfaces.http import HttpClient
46
47
48 logger = logging.getLogger(__name__)
49
50 PORT = 9999
51
52
53 class ConfigModel(ModelValidator):
54 vca_host: str
55 vca_port: int
56 vca_user: str
57 vca_password: str
58 vca_pubkey: str
59 vca_cacert: str
60 vca_cloud: str
61 vca_k8s_cloud: str
62 database_commonkey: str
63 log_level: str
64 vca_apiproxy: Optional[str]
65
66 @validator("log_level")
67 def validate_log_level(cls, v):
68 if v not in {"INFO", "DEBUG"}:
69 raise ValueError("value must be INFO or DEBUG")
70 return v
71
72
73 class LcmCharm(CharmedOsmBase):
74 def __init__(self, *args) -> NoReturn:
75 super().__init__(*args, oci_image="image")
76
77 self.kafka_client = KafkaClient(self, "kafka")
78 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
79 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
80
81 self.mongodb_client = MongoClient(self, "mongodb")
82 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
83 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
84
85 self.ro_client = HttpClient(self, "ro")
86 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
87 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
88
89 def _check_missing_dependencies(self, config: ConfigModel):
90 missing_relations = []
91
92 if self.kafka_client.is_missing_data_in_unit():
93 missing_relations.append("kafka")
94 if self.mongodb_client.is_missing_data_in_unit():
95 missing_relations.append("mongodb")
96 if self.ro_client.is_missing_data_in_app():
97 missing_relations.append("ro")
98
99 if missing_relations:
100 raise RelationsMissing(missing_relations)
101
102 def build_pod_spec(self, image_info):
103 # Validate config
104 config = ConfigModel(**dict(self.config))
105 # Check relations
106 self._check_missing_dependencies(config)
107 # Create Builder for the PodSpec
108 pod_spec_builder = PodSpecV3Builder()
109 # Build Container
110 container_builder = ContainerV3Builder(self.app.name, image_info)
111 container_builder.add_port(name=self.app.name, port=PORT)
112 container_builder.add_envs(
113 {
114 # General configuration
115 "ALLOW_ANONYMOUS_LOGIN": "yes",
116 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
117 # RO configuration
118 "OSMLCM_RO_HOST": self.ro_client.host,
119 "OSMLCM_RO_PORT": self.ro_client.port,
120 "OSMLCM_RO_TENANT": "osm",
121 # Kafka configuration
122 "OSMLCM_MESSAGE_DRIVER": "kafka",
123 "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
124 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
125 # Database configuration
126 "OSMLCM_DATABASE_DRIVER": "mongo",
127 "OSMLCM_DATABASE_URI": self.mongodb_client.connection_string,
128 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
129 # Storage configuration
130 "OSMLCM_STORAGE_DRIVER": "mongo",
131 "OSMLCM_STORAGE_PATH": "/app/storage",
132 "OSMLCM_STORAGE_COLLECTION": "files",
133 "OSMLCM_STORAGE_URI": self.mongodb_client.connection_string,
134 # VCA configuration
135 "OSMLCM_VCA_HOST": config.vca_host,
136 "OSMLCM_VCA_PORT": config.vca_port,
137 "OSMLCM_VCA_USER": config.vca_user,
138 "OSMLCM_VCA_PUBKEY": config.vca_pubkey,
139 "OSMLCM_VCA_SECRET": config.vca_password,
140 "OSMLCM_VCA_CACERT": config.vca_cacert,
141 "OSMLCM_VCA_CLOUD": config.vca_cloud,
142 "OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud,
143 }
144 )
145 if config.vca_apiproxy:
146 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
147
148 container = container_builder.build()
149 # Add container to pod spec
150 pod_spec_builder.add_container(container)
151 return pod_spec_builder.build()
152
153
154 if __name__ == "__main__":
155 main(LcmCharm)
156
157
158 # class ConfigurePodEvent(EventBase):
159 # """Configure Pod event"""
160
161 # pass
162
163
164 # class LcmEvents(CharmEvents):
165 # """LCM Events"""
166
167 # configure_pod = EventSource(ConfigurePodEvent)
168
169
170 # class LcmCharm(CharmBase):
171 # """LCM Charm."""
172
173 # state = StoredState()
174 # on = LcmEvents()
175
176 # def __init__(self, *args) -> NoReturn:
177 # """LCM Charm constructor."""
178 # super().__init__(*args)
179
180 # # Internal state initialization
181 # self.state.set_default(pod_spec=None)
182
183 # # Message bus data initialization
184 # self.state.set_default(message_host=None)
185 # self.state.set_default(message_port=None)
186
187 # # Database data initialization
188 # self.state.set_default(database_uri=None)
189
190 # # RO data initialization
191 # self.state.set_default(ro_host=None)
192 # self.state.set_default(ro_port=None)
193
194 # self.port = LCM_PORT
195 # self.image = OCIImageResource(self, "image")
196
197 # # Registering regular events
198 # self.framework.observe(self.on.start, self.configure_pod)
199 # self.framework.observe(self.on.config_changed, self.configure_pod)
200 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
201
202 # # Registering custom internal events
203 # self.framework.observe(self.on.configure_pod, self.configure_pod)
204
205 # # Registering required relation events
206 # self.framework.observe(
207 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
208 # )
209 # self.framework.observe(
210 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
211 # )
212 # self.framework.observe(
213 # self.on.ro_relation_changed, self._on_ro_relation_changed
214 # )
215
216 # # Registering required relation broken events
217 # self.framework.observe(
218 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
219 # )
220 # self.framework.observe(
221 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
222 # )
223 # self.framework.observe(
224 # self.on.ro_relation_broken, self._on_ro_relation_broken
225 # )
226
227 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
228 # """Reads information about the kafka relation.
229
230 # Args:
231 # event (EventBase): Kafka relation event.
232 # """
233 # message_host = event.relation.data[event.unit].get("host")
234 # message_port = event.relation.data[event.unit].get("port")
235
236 # if (
237 # message_host
238 # and message_port
239 # and (
240 # self.state.message_host != message_host
241 # or self.state.message_port != message_port
242 # )
243 # ):
244 # self.state.message_host = message_host
245 # self.state.message_port = message_port
246 # self.on.configure_pod.emit()
247
248 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
249 # """Clears data from kafka relation.
250
251 # Args:
252 # event (EventBase): Kafka relation event.
253 # """
254 # self.state.message_host = None
255 # self.state.message_port = None
256 # self.on.configure_pod.emit()
257
258 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
259 # """Reads information about the DB relation.
260
261 # Args:
262 # event (EventBase): DB relation event.
263 # """
264 # database_uri = event.relation.data[event.unit].get("connection_string")
265
266 # if database_uri and self.state.database_uri != database_uri:
267 # self.state.database_uri = database_uri
268 # self.on.configure_pod.emit()
269
270 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
271 # """Clears data from mongodb relation.
272
273 # Args:
274 # event (EventBase): DB relation event.
275 # """
276 # self.state.database_uri = None
277 # self.on.configure_pod.emit()
278
279 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
280 # """Reads information about the RO relation.
281
282 # Args:
283 # event (EventBase): Keystone relation event.
284 # """
285 # ro_host = event.relation.data[event.unit].get("host")
286 # ro_port = event.relation.data[event.unit].get("port")
287
288 # if (
289 # ro_host
290 # and ro_port
291 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
292 # ):
293 # self.state.ro_host = ro_host
294 # self.state.ro_port = ro_port
295 # self.on.configure_pod.emit()
296
297 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
298 # """Clears data from ro relation.
299
300 # Args:
301 # event (EventBase): Keystone relation event.
302 # """
303 # self.state.ro_host = None
304 # self.state.ro_port = None
305 # self.on.configure_pod.emit()
306
307 # def _missing_relations(self) -> str:
308 # """Checks if there missing relations.
309
310 # Returns:
311 # str: string with missing relations
312 # """
313 # data_status = {
314 # "kafka": self.state.message_host,
315 # "mongodb": self.state.database_uri,
316 # "ro": self.state.ro_host,
317 # }
318
319 # missing_relations = [k for k, v in data_status.items() if not v]
320
321 # return ", ".join(missing_relations)
322
323 # @property
324 # def relation_state(self) -> Dict[str, Any]:
325 # """Collects relation state configuration for pod spec assembly.
326
327 # Returns:
328 # Dict[str, Any]: relation state information.
329 # """
330 # relation_state = {
331 # "message_host": self.state.message_host,
332 # "message_port": self.state.message_port,
333 # "database_uri": self.state.database_uri,
334 # "ro_host": self.state.ro_host,
335 # "ro_port": self.state.ro_port,
336 # }
337
338 # return relation_state
339
340 # def configure_pod(self, event: EventBase) -> NoReturn:
341 # """Assemble the pod spec and apply it, if possible.
342
343 # Args:
344 # event (EventBase): Hook or Relation event that started the
345 # function.
346 # """
347 # if missing := self._missing_relations():
348 # self.unit.status = BlockedStatus(
349 # "Waiting for {0} relation{1}".format(
350 # missing, "s" if "," in missing else ""
351 # )
352 # )
353 # return
354
355 # if not self.unit.is_leader():
356 # self.unit.status = ActiveStatus("ready")
357 # return
358
359 # self.unit.status = MaintenanceStatus("Assembling pod spec")
360
361 # # Fetch image information
362 # try:
363 # self.unit.status = MaintenanceStatus("Fetching image information")
364 # image_info = self.image.fetch()
365 # except OCIImageResourceError:
366 # self.unit.status = BlockedStatus("Error fetching image information")
367 # return
368
369 # try:
370 # pod_spec = make_pod_spec(
371 # image_info,
372 # self.model.config,
373 # self.relation_state,
374 # self.model.app.name,
375 # self.port,
376 # )
377 # except ValueError as exc:
378 # logger.exception("Config/Relation data validation error")
379 # self.unit.status = BlockedStatus(str(exc))
380 # return
381
382 # if self.state.pod_spec != pod_spec:
383 # self.model.pod.set_spec(pod_spec)
384 # self.state.pod_spec = pod_spec
385
386 # self.unit.status = ActiveStatus("ready")
387
388
389 # if __name__ == "__main__":
390 # main(LcmCharm)