Refactoring NBI Charm to use Operator Framework
[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 message_host = event.relation.data[event.unit].get("host")
134 message_port = event.relation.data[event.unit].get("port")
135
136 if (
137 message_host
138 and message_port
139 and (
140 self.state.message_host != message_host
141 or self.state.message_port != message_port
142 )
143 ):
144 self.state.message_host = message_host
145 self.state.message_port = message_port
146 self.on.configure_pod.emit()
147
148 def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
149 """Clears data from kafka relation.
150
151 Args:
152 event (EventBase): Kafka relation event.
153 """
154 self.state.message_host = None
155 self.state.message_port = None
156 self.on.configure_pod.emit()
157
158 def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
159 """Reads information about the DB relation.
160
161 Args:
162 event (EventBase): DB relation event.
163 """
164 database_uri = event.relation.data[event.unit].get("connection_string")
165
166 if database_uri and self.state.database_uri != database_uri:
167 self.state.database_uri = database_uri
168 self.on.configure_pod.emit()
169
170 def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
171 """Clears data from mongodb relation.
172
173 Args:
174 event (EventBase): DB relation event.
175 """
176 self.state.database_uri = None
177 self.on.configure_pod.emit()
178
179 def _on_keystone_relation_changed(self, event: EventBase) -> NoReturn:
180 """Reads information about the keystone relation.
181
182 Args:
183 event (EventBase): Keystone relation event.
184 """
185 keystone_host = event.relation.data[event.unit].get("host")
186 keystone_port = event.relation.data[event.unit].get("port")
187 keystone_user_domain_name = event.relation.data[event.unit].get(
188 "user_domain_name"
189 )
190 keystone_project_domain_name = event.relation.data[event.unit].get(
191 "project_domain_name"
192 )
193 keystone_username = event.relation.data[event.unit].get("username")
194 keystone_password = event.relation.data[event.unit].get("password")
195 keystone_service = event.relation.data[event.unit].get("service")
196
197 if (
198 keystone_host
199 and keystone_port
200 and keystone_user_domain_name
201 and keystone_project_domain_name
202 and keystone_username
203 and keystone_password
204 and keystone_service
205 and (
206 self.state.keystone_host != keystone_host
207 or self.state.keystone_port != keystone_port
208 or self.state.keystone_user_domain_name != keystone_user_domain_name
209 or self.state.keystone_project_domain_name
210 != keystone_project_domain_name
211 or self.state.keystone_username != keystone_username
212 or self.state.keystone_password != keystone_password
213 or self.state.keystone_service != keystone_service
214 )
215 ):
216 self.state.keystone_host = keystone_host
217 self.state.keystone_port = keystone_port
218 self.state.keystone_user_domain_name = keystone_user_domain_name
219 self.state.keystone_project_domain_name = keystone_project_domain_name
220 self.state.keystone_username = keystone_username
221 self.state.keystone_password = keystone_password
222 self.state.keystone_service = keystone_service
223 self.on.configure_pod.emit()
224
225 def _on_keystone_relation_departed(self, event: EventBase) -> NoReturn:
226 """Clears data from keystone relation.
227
228 Args:
229 event (EventBase): Keystone relation event.
230 """
231 self.state.keystone_host = None
232 self.state.keystone_port = None
233 self.state.keystone_user_domain_name = None
234 self.state.keystone_project_domain_name = None
235 self.state.keystone_username = None
236 self.state.keystone_password = None
237 self.state.keystone_service = None
238 self.on.configure_pod.emit()
239
240 def _on_prometheus_relation_changed(self, event: EventBase) -> NoReturn:
241 """Reads information about the prometheus relation.
242
243 Args:
244 event (EventBase): Prometheus relation event.
245 """
246 prometheus_host = event.relation.data[event.unit].get("hostname")
247 prometheus_port = event.relation.data[event.unit].get("port")
248
249 if (
250 prometheus_host
251 and prometheus_port
252 and (
253 self.state.prometheus_host != prometheus_host
254 or self.state.prometheus_port != prometheus_port
255 )
256 ):
257 self.state.prometheus_host = prometheus_host
258 self.state.prometheus_port = prometheus_port
259 self.on.configure_pod.emit()
260
261 def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn:
262 """Clears data from prometheus relation.
263
264 Args:
265 event (EventBase): Prometheus relation event.
266 """
267 self.state.prometheus_host = None
268 self.state.prometheus_port = None
269 self.on.configure_pod.emit()
270
271 def _publish_nbi_info(self, event: EventBase) -> NoReturn:
272 """Publishes NBI information.
273
274 Args:
275 event (EventBase): NBI relation event.
276 """
277 if self.unit.is_leader():
278 rel_data = {
279 "host": self.model.app.name,
280 "port": str(NBI_PORT),
281 }
282 for k, v in rel_data.items():
283 event.relation.data[self.model.app][k] = v
284
285 def _missing_relations(self) -> str:
286 """Checks if there missing relations.
287
288 Returns:
289 str: string with missing relations
290 """
291 data_status = {
292 "kafka": self.state.message_host,
293 "mongodb": self.state.database_uri,
294 "prometheus": self.state.prometheus_host,
295 }
296
297 if self.model.config["auth_backend"] == "keystone":
298 data_status["keystone"] = self.state.keystone_host
299
300 missing_relations = [k for k, v in data_status.items() if not v]
301
302 return ", ".join(missing_relations)
303
304 @property
305 def relation_state(self) -> Dict[str, Any]:
306 """Collects relation state configuration for pod spec assembly.
307
308 Returns:
309 Dict[str, Any]: relation state information.
310 """
311 relation_state = {
312 "message_host": self.state.message_host,
313 "message_port": self.state.message_port,
314 "database_uri": self.state.database_uri,
315 "prometheus_host": self.state.prometheus_host,
316 "prometheus_port": self.state.prometheus_port,
317 }
318
319 if self.model.config["auth_backend"] == "keystone":
320 relation_state.update(
321 {
322 "keystone_host": self.state.keystone_host,
323 "keystone_port": self.state.keystone_port,
324 "keystone_user_domain_name": self.state.keystone_user_domain_name,
325 "keystone_project_domain_name": self.state.keystone_project_domain_name,
326 "keystone_username": self.state.keystone_username,
327 "keystone_password": self.state.keystone_password,
328 "keystone_service": self.state.keystone_service,
329 }
330 )
331
332 return relation_state
333
334 def configure_pod(self, event: EventBase) -> NoReturn:
335 """Assemble the pod spec and apply it, if possible.
336
337 Args:
338 event (EventBase): Hook or Relation event that started the
339 function.
340 """
341 if missing := self._missing_relations():
342 self.unit.status = BlockedStatus(
343 f"Waiting for {missing} relation{'s' if ',' in missing else ''}"
344 )
345 return
346
347 if not self.unit.is_leader():
348 self.unit.status = ActiveStatus("ready")
349 return
350
351 self.unit.status = MaintenanceStatus("Assembling pod spec")
352
353 # Fetch image information
354 try:
355 self.unit.status = MaintenanceStatus("Fetching image information")
356 image_info = self.image.fetch()
357 except OCIImageResourceError:
358 self.unit.status = BlockedStatus("Error fetching image information")
359 return
360
361 try:
362 pod_spec = make_pod_spec(
363 image_info,
364 self.model.config,
365 self.relation_state,
366 self.model.app.name,
367 self.port,
368 )
369 except ValidationError as exc:
370 self.unit.status = BlockedStatus(str(exc))
371 return
372
373 if self.state.pod_spec != pod_spec:
374 self.model.pod.set_spec(pod_spec)
375 self.state.pod_spec = pod_spec
376
377 self.unit.status = ActiveStatus("ready")
378
379
380 if __name__ == "__main__":
381 main(NbiCharm)