blob: f2b2ecf77da1be3a1b28273b03a50cb930e3b2ba [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
David Garciaef349d92020-12-10 21:16:12 +0100146 self.state.message_port = int(message_port)
sousaedu6248fe62020-10-13 23:46:51 +0100147 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
David Garciaef349d92020-12-10 21:16:12 +0100222 self.state.keystone_port = int(keystone_port)
sousaedu6248fe62020-10-13 23:46:51 +0100223 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
David Garciaef349d92020-12-10 21:16:12 +0100265 self.state.prometheus_port = int(prometheus_port)
sousaedu6248fe62020-10-13 23:46:51 +0100266 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 """
David Garciaef349d92020-12-10 21:16:12 +0100284 rel_data = {
285 "host": self.model.app.name,
286 "port": str(NBI_PORT),
287 }
288 for k, v in rel_data.items():
289 event.relation.data[self.unit][k] = v
sousaedu6248fe62020-10-13 23:46:51 +0100290
291 def _missing_relations(self) -> str:
292 """Checks if there missing relations.
293
294 Returns:
295 str: string with missing relations
296 """
297 data_status = {
298 "kafka": self.state.message_host,
299 "mongodb": self.state.database_uri,
300 "prometheus": self.state.prometheus_host,
301 }
302
303 if self.model.config["auth_backend"] == "keystone":
304 data_status["keystone"] = self.state.keystone_host
305
306 missing_relations = [k for k, v in data_status.items() if not v]
307
308 return ", ".join(missing_relations)
309
310 @property
311 def relation_state(self) -> Dict[str, Any]:
312 """Collects relation state configuration for pod spec assembly.
313
314 Returns:
315 Dict[str, Any]: relation state information.
316 """
317 relation_state = {
318 "message_host": self.state.message_host,
319 "message_port": self.state.message_port,
320 "database_uri": self.state.database_uri,
321 "prometheus_host": self.state.prometheus_host,
322 "prometheus_port": self.state.prometheus_port,
323 }
324
325 if self.model.config["auth_backend"] == "keystone":
326 relation_state.update(
327 {
328 "keystone_host": self.state.keystone_host,
329 "keystone_port": self.state.keystone_port,
330 "keystone_user_domain_name": self.state.keystone_user_domain_name,
331 "keystone_project_domain_name": self.state.keystone_project_domain_name,
332 "keystone_username": self.state.keystone_username,
333 "keystone_password": self.state.keystone_password,
334 "keystone_service": self.state.keystone_service,
335 }
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 f"Waiting for {missing} relation{'s' if ',' in missing else ''}"
350 )
351 return
352
353 if not self.unit.is_leader():
354 self.unit.status = ActiveStatus("ready")
355 return
356
357 self.unit.status = MaintenanceStatus("Assembling pod spec")
358
359 # Fetch image information
360 try:
361 self.unit.status = MaintenanceStatus("Fetching image information")
362 image_info = self.image.fetch()
363 except OCIImageResourceError:
364 self.unit.status = BlockedStatus("Error fetching image information")
365 return
366
367 try:
368 pod_spec = make_pod_spec(
369 image_info,
370 self.model.config,
371 self.relation_state,
372 self.model.app.name,
373 self.port,
374 )
sousaeduaf9dcc22020-12-10 01:22:09 +0000375 except ValueError as exc:
sousaedu4df5a462020-11-17 14:30:47 +0000376 logger.exception("Config/Relation data validation error")
sousaedu6248fe62020-10-13 23:46:51 +0100377 self.unit.status = BlockedStatus(str(exc))
378 return
379
380 if self.state.pod_spec != pod_spec:
381 self.model.pod.set_spec(pod_spec)
382 self.state.pod_spec = pod_spec
383
384 self.unit.status = ActiveStatus("ready")
385
386
387if __name__ == "__main__":
388 main(NbiCharm)