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