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 pydantic
import ValidationError
25 from typing
import Any
, Dict
, NoReturn
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 LcmEvents(CharmEvents
):
49 configure_pod
= EventSource(ConfigurePodEvent
)
52 class LcmCharm(CharmBase
):
58 def __init__(self
, *args
) -> NoReturn
:
59 """LCM 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 # RO data initialization
73 self
.state
.set_default(ro_host
=None)
74 self
.state
.set_default(ro_port
=None)
77 self
.image
= OCIImageResource(self
, "image")
79 # Registering regular events
80 self
.framework
.observe(self
.on
.start
, self
.configure_pod
)
81 self
.framework
.observe(self
.on
.config_changed
, self
.configure_pod
)
82 self
.framework
.observe(self
.on
.upgrade_charm
, self
.configure_pod
)
84 # Registering custom internal events
85 self
.framework
.observe(self
.on
.configure_pod
, self
.configure_pod
)
87 # Registering required relation events
88 self
.framework
.observe(
89 self
.on
.kafka_relation_changed
, self
._on
_kafka
_relation
_changed
91 self
.framework
.observe(
92 self
.on
.mongodb_relation_changed
, self
._on
_mongodb
_relation
_changed
94 self
.framework
.observe(
95 self
.on
.ro_relation_changed
, self
._on
_ro
_relation
_changed
98 # Registering required relation departed events
99 self
.framework
.observe(
100 self
.on
.kafka_relation_departed
, self
._on
_kafka
_relation
_departed
102 self
.framework
.observe(
103 self
.on
.mongodb_relation_departed
, self
._on
_mongodb
_relation
_departed
105 self
.framework
.observe(
106 self
.on
.ro_relation_departed
, self
._on
_ro
_relation
_departed
109 def _on_kafka_relation_changed(self
, event
: EventBase
) -> NoReturn
:
110 """Reads information about the kafka relation.
113 event (EventBase): Kafka relation event.
115 data_loc
= event
.unit
if event
.unit
else event
.app
117 message_host
= event
.relation
.data
[data_loc
].get("host")
118 message_port
= event
.relation
.data
[data_loc
].get("port")
124 self
.state
.message_host
!= message_host
125 or self
.state
.message_port
!= message_port
128 self
.state
.message_host
= message_host
129 self
.state
.message_port
= message_port
130 self
.on
.configure_pod
.emit()
132 def _on_kafka_relation_departed(self
, event
: EventBase
) -> NoReturn
:
133 """Clears data from kafka relation.
136 event (EventBase): Kafka relation event.
138 self
.state
.message_host
= None
139 self
.state
.message_port
= None
140 self
.on
.configure_pod
.emit()
142 def _on_mongodb_relation_changed(self
, event
: EventBase
) -> NoReturn
:
143 """Reads information about the DB relation.
146 event (EventBase): DB relation event.
148 data_loc
= event
.unit
if event
.unit
else event
.app
150 database_uri
= event
.relation
.data
[data_loc
].get("connection_string")
152 if database_uri
and self
.state
.database_uri
!= database_uri
:
153 self
.state
.database_uri
= database_uri
154 self
.on
.configure_pod
.emit()
156 def _on_mongodb_relation_departed(self
, event
: EventBase
) -> NoReturn
:
157 """Clears data from mongodb relation.
160 event (EventBase): DB relation event.
162 self
.state
.database_uri
= None
163 self
.on
.configure_pod
.emit()
165 def _on_ro_relation_changed(self
, event
: EventBase
) -> NoReturn
:
166 """Reads information about the RO relation.
169 event (EventBase): Keystone relation event.
171 data_loc
= event
.unit
if event
.unit
else event
.app
173 ro_host
= event
.relation
.data
[data_loc
].get("host")
174 ro_port
= event
.relation
.data
[data_loc
].get("port")
179 and (self
.state
.ro_host
!= ro_host
or self
.state
.ro_port
!= ro_port
)
181 self
.state
.ro_host
= ro_host
182 self
.state
.ro_port
= ro_port
183 self
.on
.configure_pod
.emit()
185 def _on_ro_relation_departed(self
, event
: EventBase
) -> NoReturn
:
186 """Clears data from ro relation.
189 event (EventBase): Keystone relation event.
191 self
.state
.ro_host
= None
192 self
.state
.ro_port
= None
193 self
.on
.configure_pod
.emit()
195 def _missing_relations(self
) -> str:
196 """Checks if there missing relations.
199 str: string with missing relations
202 "kafka": self
.state
.message_host
,
203 "mongodb": self
.state
.database_uri
,
204 "ro": self
.state
.ro_host
,
207 missing_relations
= [k
for k
, v
in data_status
.items() if not v
]
209 return ", ".join(missing_relations
)
212 def relation_state(self
) -> Dict
[str, Any
]:
213 """Collects relation state configuration for pod spec assembly.
216 Dict[str, Any]: relation state information.
219 "message_host": self
.state
.message_host
,
220 "message_port": self
.state
.message_port
,
221 "database_uri": self
.state
.database_uri
,
222 "ro_host": self
.state
.ro_host
,
223 "ro_port": self
.state
.ro_port
,
226 return relation_state
228 def configure_pod(self
, event
: EventBase
) -> NoReturn
:
229 """Assemble the pod spec and apply it, if possible.
232 event (EventBase): Hook or Relation event that started the
235 if missing
:= self
._missing
_relations
():
236 self
.unit
.status
= BlockedStatus(
237 "Waiting for {0} relation{1}".format(
238 missing
, "s" if "," in missing
else ""
243 if not self
.unit
.is_leader():
244 self
.unit
.status
= ActiveStatus("ready")
247 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
249 # Fetch image information
251 self
.unit
.status
= MaintenanceStatus("Fetching image information")
252 image_info
= self
.image
.fetch()
253 except OCIImageResourceError
:
254 self
.unit
.status
= BlockedStatus("Error fetching image information")
258 pod_spec
= make_pod_spec(
265 except ValidationError
as exc
:
266 logger
.exception("Config/Relation data validation error")
267 self
.unit
.status
= BlockedStatus(str(exc
))
270 if self
.state
.pod_spec
!= pod_spec
:
271 self
.model
.pod
.set_spec(pod_spec
)
272 self
.state
.pod_spec
= pod_spec
274 self
.unit
.status
= ActiveStatus("ready")
277 if __name__
== "__main__":