848b53d9fd803c97a1c8b1a2347ecce5b8be0b9d
[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
26 from ops.charm import CharmBase, CharmEvents
27 from ops.framework import EventBase, EventSource, StoredState
28 from ops.main import main
29 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
30 from oci_image import OCIImageResource, OCIImageResourceError
31
32 from pod_spec import make_pod_spec
33
34 logger = logging.getLogger(__name__)
35
36 NBI_PORT = 9999
37
38
39 class ConfigurePodEvent(EventBase):
40 """Configure Pod event"""
41
42 pass
43
44
45 class NbiEvents(CharmEvents):
46 """NBI Events"""
47
48 configure_pod = EventSource(ConfigurePodEvent)
49
50
51 class 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 """
132 message_host = event.relation.data[event.unit].get("host")
133 message_port = event.relation.data[event.unit].get("port")
134
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
144 self.state.message_port = int(message_port)
145 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 """
163 database_uri = event.relation.data[event.unit].get("connection_string")
164
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 """
184 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(
187 "user_domain_name"
188 )
189 keystone_project_domain_name = event.relation.data[event.unit].get(
190 "project_domain_name"
191 )
192 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")
195
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
216 self.state.keystone_port = int(keystone_port)
217 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 """
245 prometheus_host = event.relation.data[event.unit].get("hostname")
246 prometheus_port = event.relation.data[event.unit].get("port")
247
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
257 self.state.prometheus_port = int(prometheus_port)
258 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 """
276 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
282
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 )
367 except ValueError as exc:
368 logger.exception("Config/Relation data validation error")
369 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
379 if __name__ == "__main__":
380 main(NbiCharm)