blob: 6db99be6f23a6619e0f2ddb661a4115b0bc0fa06 [file] [log] [blame]
sousaedu6248fe62020-10-13 23:46:51 +01001#!/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
23import logging
24from typing import Any, Dict, NoReturn
sousaedu6248fe62020-10-13 23:46:51 +010025
26from ops.charm import CharmBase, CharmEvents
27from ops.framework import EventBase, EventSource, StoredState
28from ops.main import main
29from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
30from oci_image import OCIImageResource, OCIImageResourceError
31
32from pod_spec import make_pod_spec
33
sousaedu4df5a462020-11-17 14:30:47 +000034logger = logging.getLogger(__name__)
sousaedu6248fe62020-10-13 23:46:51 +010035
36NBI_PORT = 9999
37
38
39class ConfigurePodEvent(EventBase):
40 """Configure Pod event"""
41
42 pass
43
44
45class NbiEvents(CharmEvents):
46 """NBI Events"""
47
48 configure_pod = EventSource(ConfigurePodEvent)
49
50
51class 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 """
sousaedu4df5a462020-11-17 14:30:47 +0000132 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")
sousaedu6248fe62020-10-13 23:46:51 +0100136
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 """
sousaedu4df5a462020-11-17 14:30:47 +0000165 data_loc = event.unit if event.unit else event.app
166
167 database_uri = event.relation.data[data_loc].get("connection_string")
sousaedu6248fe62020-10-13 23:46:51 +0100168
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 """
sousaedu4df5a462020-11-17 14:30:47 +0000188 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(
sousaedu6248fe62020-10-13 23:46:51 +0100193 "user_domain_name"
194 )
sousaedu4df5a462020-11-17 14:30:47 +0000195 keystone_project_domain_name = event.relation.data[data_loc].get(
sousaedu6248fe62020-10-13 23:46:51 +0100196 "project_domain_name"
197 )
sousaedu4df5a462020-11-17 14:30:47 +0000198 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")
sousaedu6248fe62020-10-13 23:46:51 +0100201
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 """
sousaedu4df5a462020-11-17 14:30:47 +0000251 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")
sousaedu6248fe62020-10-13 23:46:51 +0100255
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 )
sousaeduaf9dcc22020-12-10 01:22:09 +0000376 except ValueError as exc:
sousaedu4df5a462020-11-17 14:30:47 +0000377 logger.exception("Config/Relation data validation error")
sousaedu6248fe62020-10-13 23:46:51 +0100378 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
388if __name__ == "__main__":
389 main(NbiCharm)