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