2 # Copyright 2020 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
24 from typing
import Any
, Dict
, NoReturn
25 from pydantic
import ValidationError
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
33 from pod_spec
import make_pod_spec
35 logger
= logging
.getLogger(__name__
)
40 class ConfigurePodEvent(EventBase
):
41 """Configure Pod event"""
46 class NbiEvents(CharmEvents
):
49 configure_pod
= EventSource(ConfigurePodEvent
)
52 class NbiCharm(CharmBase
):
58 def __init__(self
, *args
) -> NoReturn
:
59 """NBI Charm constructor."""
60 super().__init
__(*args
)
62 # Internal state initialization
63 self
.state
.set_default(pod_spec
=None)
65 # Message bus data initialization
66 self
.state
.set_default(message_host
=None)
67 self
.state
.set_default(message_port
=None)
69 # Database data initialization
70 self
.state
.set_default(database_uri
=None)
72 # Prometheus data initialization
73 self
.state
.set_default(prometheus_host
=None)
74 self
.state
.set_default(prometheus_port
=None)
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)
86 self
.image
= OCIImageResource(self
, "image")
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
)
93 # Registering custom internal events
94 self
.framework
.observe(self
.on
.configure_pod
, self
.configure_pod
)
96 # Registering required relation changed events
97 self
.framework
.observe(
98 self
.on
.kafka_relation_changed
, self
._on
_kafka
_relation
_changed
100 self
.framework
.observe(
101 self
.on
.mongodb_relation_changed
, self
._on
_mongodb
_relation
_changed
103 self
.framework
.observe(
104 self
.on
.keystone_relation_changed
, self
._on
_keystone
_relation
_changed
106 self
.framework
.observe(
107 self
.on
.prometheus_relation_changed
, self
._on
_prometheus
_relation
_changed
110 # Registering required relation departed events
111 self
.framework
.observe(
112 self
.on
.kafka_relation_departed
, self
._on
_kafka
_relation
_departed
114 self
.framework
.observe(
115 self
.on
.mongodb_relation_departed
, self
._on
_mongodb
_relation
_departed
117 self
.framework
.observe(
118 self
.on
.keystone_relation_departed
, self
._on
_keystone
_relation
_departed
120 self
.framework
.observe(
121 self
.on
.prometheus_relation_departed
, self
._on
_prometheus
_relation
_departed
124 # Registering provided relation events
125 self
.framework
.observe(self
.on
.nbi_relation_joined
, self
._publish
_nbi
_info
)
127 def _on_kafka_relation_changed(self
, event
: EventBase
) -> NoReturn
:
128 """Reads information about the kafka relation.
131 event (EventBase): Kafka relation event.
133 data_loc
= event
.unit
if event
.unit
else event
.app
135 message_host
= event
.relation
.data
[data_loc
].get("host")
136 message_port
= event
.relation
.data
[data_loc
].get("port")
142 self
.state
.message_host
!= message_host
143 or self
.state
.message_port
!= message_port
146 self
.state
.message_host
= message_host
147 self
.state
.message_port
= message_port
148 self
.on
.configure_pod
.emit()
150 def _on_kafka_relation_departed(self
, event
: EventBase
) -> NoReturn
:
151 """Clears data from kafka relation.
154 event (EventBase): Kafka relation event.
156 self
.state
.message_host
= None
157 self
.state
.message_port
= None
158 self
.on
.configure_pod
.emit()
160 def _on_mongodb_relation_changed(self
, event
: EventBase
) -> NoReturn
:
161 """Reads information about the DB relation.
164 event (EventBase): DB relation event.
166 data_loc
= event
.unit
if event
.unit
else event
.app
168 database_uri
= event
.relation
.data
[data_loc
].get("connection_string")
170 if database_uri
and self
.state
.database_uri
!= database_uri
:
171 self
.state
.database_uri
= database_uri
172 self
.on
.configure_pod
.emit()
174 def _on_mongodb_relation_departed(self
, event
: EventBase
) -> NoReturn
:
175 """Clears data from mongodb relation.
178 event (EventBase): DB relation event.
180 self
.state
.database_uri
= None
181 self
.on
.configure_pod
.emit()
183 def _on_keystone_relation_changed(self
, event
: EventBase
) -> NoReturn
:
184 """Reads information about the keystone relation.
187 event (EventBase): Keystone relation event.
189 data_loc
= event
.unit
if event
.unit
else event
.app
191 keystone_host
= event
.relation
.data
[data_loc
].get("host")
192 keystone_port
= event
.relation
.data
[data_loc
].get("port")
193 keystone_user_domain_name
= event
.relation
.data
[data_loc
].get(
196 keystone_project_domain_name
= event
.relation
.data
[data_loc
].get(
197 "project_domain_name"
199 keystone_username
= event
.relation
.data
[data_loc
].get("username")
200 keystone_password
= event
.relation
.data
[data_loc
].get("password")
201 keystone_service
= event
.relation
.data
[data_loc
].get("service")
206 and keystone_user_domain_name
207 and keystone_project_domain_name
208 and keystone_username
209 and keystone_password
212 self
.state
.keystone_host
!= keystone_host
213 or self
.state
.keystone_port
!= keystone_port
214 or self
.state
.keystone_user_domain_name
!= keystone_user_domain_name
215 or self
.state
.keystone_project_domain_name
216 != keystone_project_domain_name
217 or self
.state
.keystone_username
!= keystone_username
218 or self
.state
.keystone_password
!= keystone_password
219 or self
.state
.keystone_service
!= keystone_service
222 self
.state
.keystone_host
= keystone_host
223 self
.state
.keystone_port
= keystone_port
224 self
.state
.keystone_user_domain_name
= keystone_user_domain_name
225 self
.state
.keystone_project_domain_name
= keystone_project_domain_name
226 self
.state
.keystone_username
= keystone_username
227 self
.state
.keystone_password
= keystone_password
228 self
.state
.keystone_service
= keystone_service
229 self
.on
.configure_pod
.emit()
231 def _on_keystone_relation_departed(self
, event
: EventBase
) -> NoReturn
:
232 """Clears data from keystone relation.
235 event (EventBase): Keystone relation event.
237 self
.state
.keystone_host
= None
238 self
.state
.keystone_port
= None
239 self
.state
.keystone_user_domain_name
= None
240 self
.state
.keystone_project_domain_name
= None
241 self
.state
.keystone_username
= None
242 self
.state
.keystone_password
= None
243 self
.state
.keystone_service
= None
244 self
.on
.configure_pod
.emit()
246 def _on_prometheus_relation_changed(self
, event
: EventBase
) -> NoReturn
:
247 """Reads information about the prometheus relation.
250 event (EventBase): Prometheus relation event.
252 data_loc
= event
.unit
if event
.unit
else event
.app
254 prometheus_host
= event
.relation
.data
[data_loc
].get("hostname")
255 prometheus_port
= event
.relation
.data
[data_loc
].get("port")
261 self
.state
.prometheus_host
!= prometheus_host
262 or self
.state
.prometheus_port
!= prometheus_port
265 self
.state
.prometheus_host
= prometheus_host
266 self
.state
.prometheus_port
= prometheus_port
267 self
.on
.configure_pod
.emit()
269 def _on_prometheus_relation_departed(self
, event
: EventBase
) -> NoReturn
:
270 """Clears data from prometheus relation.
273 event (EventBase): Prometheus relation event.
275 self
.state
.prometheus_host
= None
276 self
.state
.prometheus_port
= None
277 self
.on
.configure_pod
.emit()
279 def _publish_nbi_info(self
, event
: EventBase
) -> NoReturn
:
280 """Publishes NBI information.
283 event (EventBase): NBI relation event.
285 if self
.unit
.is_leader():
287 "host": self
.model
.app
.name
,
288 "port": str(NBI_PORT
),
290 for k
, v
in rel_data
.items():
291 event
.relation
.data
[self
.model
.app
][k
] = v
293 def _missing_relations(self
) -> str:
294 """Checks if there missing relations.
297 str: string with missing relations
300 "kafka": self
.state
.message_host
,
301 "mongodb": self
.state
.database_uri
,
302 "prometheus": self
.state
.prometheus_host
,
305 if self
.model
.config
["auth_backend"] == "keystone":
306 data_status
["keystone"] = self
.state
.keystone_host
308 missing_relations
= [k
for k
, v
in data_status
.items() if not v
]
310 return ", ".join(missing_relations
)
313 def relation_state(self
) -> Dict
[str, Any
]:
314 """Collects relation state configuration for pod spec assembly.
317 Dict[str, Any]: relation state information.
320 "message_host": self
.state
.message_host
,
321 "message_port": self
.state
.message_port
,
322 "database_uri": self
.state
.database_uri
,
323 "prometheus_host": self
.state
.prometheus_host
,
324 "prometheus_port": self
.state
.prometheus_port
,
327 if self
.model
.config
["auth_backend"] == "keystone":
328 relation_state
.update(
330 "keystone_host": self
.state
.keystone_host
,
331 "keystone_port": self
.state
.keystone_port
,
332 "keystone_user_domain_name": self
.state
.keystone_user_domain_name
,
333 "keystone_project_domain_name": self
.state
.keystone_project_domain_name
,
334 "keystone_username": self
.state
.keystone_username
,
335 "keystone_password": self
.state
.keystone_password
,
336 "keystone_service": self
.state
.keystone_service
,
340 return relation_state
342 def configure_pod(self
, event
: EventBase
) -> NoReturn
:
343 """Assemble the pod spec and apply it, if possible.
346 event (EventBase): Hook or Relation event that started the
349 if missing
:= self
._missing
_relations
():
350 self
.unit
.status
= BlockedStatus(
351 f
"Waiting for {missing} relation{'s' if ',' in missing else ''}"
355 if not self
.unit
.is_leader():
356 self
.unit
.status
= ActiveStatus("ready")
359 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
361 # Fetch image information
363 self
.unit
.status
= MaintenanceStatus("Fetching image information")
364 image_info
= self
.image
.fetch()
365 except OCIImageResourceError
:
366 self
.unit
.status
= BlockedStatus("Error fetching image information")
370 pod_spec
= make_pod_spec(
377 except ValidationError
as exc
:
378 logger
.exception("Config/Relation data validation error")
379 self
.unit
.status
= BlockedStatus(str(exc
))
382 if self
.state
.pod_spec
!= pod_spec
:
383 self
.model
.pod
.set_spec(pod_spec
)
384 self
.state
.pod_spec
= pod_spec
386 self
.unit
.status
= ActiveStatus("ready")
389 if __name__
== "__main__":