Removing pydantic from NBI charm
[osm/devops.git] / installers / charm / nbi / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2020 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 import logging
24 from typing import Any, Dict, NoReturn
25
26 from ops.charm import CharmBase, CharmEvents
27 from ops.framework import EventBase, EventSource, StoredState
28 from ops.main import main
29 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
30 from oci_image import OCIImageResource, OCIImageResourceError
31
32 from pod_spec import make_pod_spec
33
34 logger = logging.getLogger(__name__)
35
36 NBI_PORT = 9999
37
38
39 class ConfigurePodEvent(EventBase):
40 """Configure Pod event"""
41
42 pass
43
44
45 class NbiEvents(CharmEvents):
46 """NBI Events"""
47
48 configure_pod = EventSource(ConfigurePodEvent)
49
50
51 class NbiCharm(CharmBase):
52 """NBI Charm."""
53
54 state = StoredState()
55 on = NbiEvents()
56
57 def __init__(self, *args) -> NoReturn:
58 """NBI Charm constructor."""
59 super().__init__(*args)
60
61 # Internal state initialization
62 self.state.set_default(pod_spec=None)
63
64 # Message bus data initialization
65 self.state.set_default(message_host=None)
66 self.state.set_default(message_port=None)
67
68 # Database data initialization
69 self.state.set_default(database_uri=None)
70
71 # Prometheus data initialization
72 self.state.set_default(prometheus_host=None)
73 self.state.set_default(prometheus_port=None)
74
75 # Keystone data initialization
76 self.state.set_default(keystone_host=None)
77 self.state.set_default(keystone_port=None)
78 self.state.set_default(keystone_user_domain_name=None)
79 self.state.set_default(keystone_project_domain_name=None)
80 self.state.set_default(keystone_username=None)
81 self.state.set_default(keystone_password=None)
82 self.state.set_default(keystone_service=None)
83
84 self.port = NBI_PORT
85 self.image = OCIImageResource(self, "image")
86
87 # Registering regular events
88 self.framework.observe(self.on.start, self.configure_pod)
89 self.framework.observe(self.on.config_changed, self.configure_pod)
90 self.framework.observe(self.on.upgrade_charm, self.configure_pod)
91
92 # Registering custom internal events
93 self.framework.observe(self.on.configure_pod, self.configure_pod)
94
95 # Registering required relation changed events
96 self.framework.observe(
97 self.on.kafka_relation_changed, self._on_kafka_relation_changed
98 )
99 self.framework.observe(
100 self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
101 )
102 self.framework.observe(
103 self.on.keystone_relation_changed, self._on_keystone_relation_changed
104 )
105 self.framework.observe(
106 self.on.prometheus_relation_changed, self._on_prometheus_relation_changed
107 )
108
109 # Registering required relation departed events
110 self.framework.observe(
111 self.on.kafka_relation_departed, self._on_kafka_relation_departed
112 )
113 self.framework.observe(
114 self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
115 )
116 self.framework.observe(
117 self.on.keystone_relation_departed, self._on_keystone_relation_departed
118 )
119 self.framework.observe(
120 self.on.prometheus_relation_departed, self._on_prometheus_relation_departed
121 )
122
123 # Registering provided relation events
124 self.framework.observe(self.on.nbi_relation_joined, self._publish_nbi_info)
125
126 def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
127 """Reads information about the kafka relation.
128
129 Args:
130 event (EventBase): Kafka relation event.
131 """
132 data_loc = event.unit if event.unit else event.app
133
134 message_host = event.relation.data[data_loc].get("host")
135 message_port = event.relation.data[data_loc].get("port")
136
137 if (
138 message_host
139 and message_port
140 and (
141 self.state.message_host != message_host
142 or self.state.message_port != message_port
143 )
144 ):
145 self.state.message_host = message_host
146 self.state.message_port = message_port
147 self.on.configure_pod.emit()
148
149 def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
150 """Clears data from kafka relation.
151
152 Args:
153 event (EventBase): Kafka relation event.
154 """
155 self.state.message_host = None
156 self.state.message_port = None
157 self.on.configure_pod.emit()
158
159 def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
160 """Reads information about the DB relation.
161
162 Args:
163 event (EventBase): DB relation event.
164 """
165 data_loc = event.unit if event.unit else event.app
166
167 database_uri = event.relation.data[data_loc].get("connection_string")
168
169 if database_uri and self.state.database_uri != database_uri:
170 self.state.database_uri = database_uri
171 self.on.configure_pod.emit()
172
173 def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
174 """Clears data from mongodb relation.
175
176 Args:
177 event (EventBase): DB relation event.
178 """
179 self.state.database_uri = None
180 self.on.configure_pod.emit()
181
182 def _on_keystone_relation_changed(self, event: EventBase) -> NoReturn:
183 """Reads information about the keystone relation.
184
185 Args:
186 event (EventBase): Keystone relation event.
187 """
188 data_loc = event.unit if event.unit else event.app
189
190 keystone_host = event.relation.data[data_loc].get("host")
191 keystone_port = event.relation.data[data_loc].get("port")
192 keystone_user_domain_name = event.relation.data[data_loc].get(
193 "user_domain_name"
194 )
195 keystone_project_domain_name = event.relation.data[data_loc].get(
196 "project_domain_name"
197 )
198 keystone_username = event.relation.data[data_loc].get("username")
199 keystone_password = event.relation.data[data_loc].get("password")
200 keystone_service = event.relation.data[data_loc].get("service")
201
202 if (
203 keystone_host
204 and keystone_port
205 and keystone_user_domain_name
206 and keystone_project_domain_name
207 and keystone_username
208 and keystone_password
209 and keystone_service
210 and (
211 self.state.keystone_host != keystone_host
212 or self.state.keystone_port != keystone_port
213 or self.state.keystone_user_domain_name != keystone_user_domain_name
214 or self.state.keystone_project_domain_name
215 != keystone_project_domain_name
216 or self.state.keystone_username != keystone_username
217 or self.state.keystone_password != keystone_password
218 or self.state.keystone_service != keystone_service
219 )
220 ):
221 self.state.keystone_host = keystone_host
222 self.state.keystone_port = keystone_port
223 self.state.keystone_user_domain_name = keystone_user_domain_name
224 self.state.keystone_project_domain_name = keystone_project_domain_name
225 self.state.keystone_username = keystone_username
226 self.state.keystone_password = keystone_password
227 self.state.keystone_service = keystone_service
228 self.on.configure_pod.emit()
229
230 def _on_keystone_relation_departed(self, event: EventBase) -> NoReturn:
231 """Clears data from keystone relation.
232
233 Args:
234 event (EventBase): Keystone relation event.
235 """
236 self.state.keystone_host = None
237 self.state.keystone_port = None
238 self.state.keystone_user_domain_name = None
239 self.state.keystone_project_domain_name = None
240 self.state.keystone_username = None
241 self.state.keystone_password = None
242 self.state.keystone_service = None
243 self.on.configure_pod.emit()
244
245 def _on_prometheus_relation_changed(self, event: EventBase) -> NoReturn:
246 """Reads information about the prometheus relation.
247
248 Args:
249 event (EventBase): Prometheus relation event.
250 """
251 data_loc = event.unit if event.unit else event.app
252
253 prometheus_host = event.relation.data[data_loc].get("hostname")
254 prometheus_port = event.relation.data[data_loc].get("port")
255
256 if (
257 prometheus_host
258 and prometheus_port
259 and (
260 self.state.prometheus_host != prometheus_host
261 or self.state.prometheus_port != prometheus_port
262 )
263 ):
264 self.state.prometheus_host = prometheus_host
265 self.state.prometheus_port = prometheus_port
266 self.on.configure_pod.emit()
267
268 def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn:
269 """Clears data from prometheus relation.
270
271 Args:
272 event (EventBase): Prometheus relation event.
273 """
274 self.state.prometheus_host = None
275 self.state.prometheus_port = None
276 self.on.configure_pod.emit()
277
278 def _publish_nbi_info(self, event: EventBase) -> NoReturn:
279 """Publishes NBI information.
280
281 Args:
282 event (EventBase): NBI relation event.
283 """
284 if self.unit.is_leader():
285 rel_data = {
286 "host": self.model.app.name,
287 "port": str(NBI_PORT),
288 }
289 for k, v in rel_data.items():
290 event.relation.data[self.model.app][k] = v
291
292 def _missing_relations(self) -> str:
293 """Checks if there missing relations.
294
295 Returns:
296 str: string with missing relations
297 """
298 data_status = {
299 "kafka": self.state.message_host,
300 "mongodb": self.state.database_uri,
301 "prometheus": self.state.prometheus_host,
302 }
303
304 if self.model.config["auth_backend"] == "keystone":
305 data_status["keystone"] = self.state.keystone_host
306
307 missing_relations = [k for k, v in data_status.items() if not v]
308
309 return ", ".join(missing_relations)
310
311 @property
312 def relation_state(self) -> Dict[str, Any]:
313 """Collects relation state configuration for pod spec assembly.
314
315 Returns:
316 Dict[str, Any]: relation state information.
317 """
318 relation_state = {
319 "message_host": self.state.message_host,
320 "message_port": self.state.message_port,
321 "database_uri": self.state.database_uri,
322 "prometheus_host": self.state.prometheus_host,
323 "prometheus_port": self.state.prometheus_port,
324 }
325
326 if self.model.config["auth_backend"] == "keystone":
327 relation_state.update(
328 {
329 "keystone_host": self.state.keystone_host,
330 "keystone_port": self.state.keystone_port,
331 "keystone_user_domain_name": self.state.keystone_user_domain_name,
332 "keystone_project_domain_name": self.state.keystone_project_domain_name,
333 "keystone_username": self.state.keystone_username,
334 "keystone_password": self.state.keystone_password,
335 "keystone_service": self.state.keystone_service,
336 }
337 )
338
339 return relation_state
340
341 def configure_pod(self, event: EventBase) -> NoReturn:
342 """Assemble the pod spec and apply it, if possible.
343
344 Args:
345 event (EventBase): Hook or Relation event that started the
346 function.
347 """
348 if missing := self._missing_relations():
349 self.unit.status = BlockedStatus(
350 f"Waiting for {missing} relation{'s' if ',' in missing else ''}"
351 )
352 return
353
354 if not self.unit.is_leader():
355 self.unit.status = ActiveStatus("ready")
356 return
357
358 self.unit.status = MaintenanceStatus("Assembling pod spec")
359
360 # Fetch image information
361 try:
362 self.unit.status = MaintenanceStatus("Fetching image information")
363 image_info = self.image.fetch()
364 except OCIImageResourceError:
365 self.unit.status = BlockedStatus("Error fetching image information")
366 return
367
368 try:
369 pod_spec = make_pod_spec(
370 image_info,
371 self.model.config,
372 self.relation_state,
373 self.model.app.name,
374 self.port,
375 )
376 except ValueError as exc:
377 logger.exception("Config/Relation data validation error")
378 self.unit.status = BlockedStatus(str(exc))
379 return
380
381 if self.state.pod_spec != pod_spec:
382 self.model.pod.set_spec(pod_spec)
383 self.state.pod_spec = pod_spec
384
385 self.unit.status = ActiveStatus("ready")
386
387
388 if __name__ == "__main__":
389 main(NbiCharm)