d65bdcfa35c8be6c5ebb4383de6c59bb06597ef8
[osm/devops.git] / installers / charm / lcm / 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 pydantic import ValidationError
25 from typing import Any, Dict, NoReturn
26
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
32
33 from pod_spec import make_pod_spec
34
35 logger = logging.getLogger(__name__)
36
37 LCM_PORT = 9999
38
39
40 class ConfigurePodEvent(EventBase):
41 """Configure Pod event"""
42
43 pass
44
45
46 class LcmEvents(CharmEvents):
47 """LCM Events"""
48
49 configure_pod = EventSource(ConfigurePodEvent)
50
51
52 class LcmCharm(CharmBase):
53 """LCM Charm."""
54
55 state = StoredState()
56 on = LcmEvents()
57
58 def __init__(self, *args) -> NoReturn:
59 """LCM Charm constructor."""
60 super().__init__(*args)
61
62 # Internal state initialization
63 self.state.set_default(pod_spec=None)
64
65 # Message bus data initialization
66 self.state.set_default(message_host=None)
67 self.state.set_default(message_port=None)
68
69 # Database data initialization
70 self.state.set_default(database_uri=None)
71
72 # RO data initialization
73 self.state.set_default(ro_host=None)
74 self.state.set_default(ro_port=None)
75
76 self.port = LCM_PORT
77 self.image = OCIImageResource(self, "image")
78
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)
83
84 # Registering custom internal events
85 self.framework.observe(self.on.configure_pod, self.configure_pod)
86
87 # Registering required relation events
88 self.framework.observe(
89 self.on.kafka_relation_changed, self._on_kafka_relation_changed
90 )
91 self.framework.observe(
92 self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
93 )
94 self.framework.observe(
95 self.on.ro_relation_changed, self._on_ro_relation_changed
96 )
97
98 # Registering required relation departed events
99 self.framework.observe(
100 self.on.kafka_relation_departed, self._on_kafka_relation_departed
101 )
102 self.framework.observe(
103 self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
104 )
105 self.framework.observe(
106 self.on.ro_relation_departed, self._on_ro_relation_departed
107 )
108
109 def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
110 """Reads information about the kafka relation.
111
112 Args:
113 event (EventBase): Kafka relation event.
114 """
115 data_loc = event.unit if event.unit else event.app
116
117 message_host = event.relation.data[data_loc].get("host")
118 message_port = event.relation.data[data_loc].get("port")
119
120 if (
121 message_host
122 and message_port
123 and (
124 self.state.message_host != message_host
125 or self.state.message_port != message_port
126 )
127 ):
128 self.state.message_host = message_host
129 self.state.message_port = message_port
130 self.on.configure_pod.emit()
131
132 def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
133 """Clears data from kafka relation.
134
135 Args:
136 event (EventBase): Kafka relation event.
137 """
138 self.state.message_host = None
139 self.state.message_port = None
140 self.on.configure_pod.emit()
141
142 def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
143 """Reads information about the DB relation.
144
145 Args:
146 event (EventBase): DB relation event.
147 """
148 data_loc = event.unit if event.unit else event.app
149
150 database_uri = event.relation.data[data_loc].get("connection_string")
151
152 if database_uri and self.state.database_uri != database_uri:
153 self.state.database_uri = database_uri
154 self.on.configure_pod.emit()
155
156 def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
157 """Clears data from mongodb relation.
158
159 Args:
160 event (EventBase): DB relation event.
161 """
162 self.state.database_uri = None
163 self.on.configure_pod.emit()
164
165 def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
166 """Reads information about the RO relation.
167
168 Args:
169 event (EventBase): Keystone relation event.
170 """
171 data_loc = event.unit if event.unit else event.app
172
173 ro_host = event.relation.data[data_loc].get("host")
174 ro_port = event.relation.data[data_loc].get("port")
175
176 if (
177 ro_host
178 and ro_port
179 and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
180 ):
181 self.state.ro_host = ro_host
182 self.state.ro_port = ro_port
183 self.on.configure_pod.emit()
184
185 def _on_ro_relation_departed(self, event: EventBase) -> NoReturn:
186 """Clears data from ro relation.
187
188 Args:
189 event (EventBase): Keystone relation event.
190 """
191 self.state.ro_host = None
192 self.state.ro_port = None
193 self.on.configure_pod.emit()
194
195 def _missing_relations(self) -> str:
196 """Checks if there missing relations.
197
198 Returns:
199 str: string with missing relations
200 """
201 data_status = {
202 "kafka": self.state.message_host,
203 "mongodb": self.state.database_uri,
204 "ro": self.state.ro_host,
205 }
206
207 missing_relations = [k for k, v in data_status.items() if not v]
208
209 return ", ".join(missing_relations)
210
211 @property
212 def relation_state(self) -> Dict[str, Any]:
213 """Collects relation state configuration for pod spec assembly.
214
215 Returns:
216 Dict[str, Any]: relation state information.
217 """
218 relation_state = {
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,
224 }
225
226 return relation_state
227
228 def configure_pod(self, event: EventBase) -> NoReturn:
229 """Assemble the pod spec and apply it, if possible.
230
231 Args:
232 event (EventBase): Hook or Relation event that started the
233 function.
234 """
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 ""
239 )
240 )
241 return
242
243 if not self.unit.is_leader():
244 self.unit.status = ActiveStatus("ready")
245 return
246
247 self.unit.status = MaintenanceStatus("Assembling pod spec")
248
249 # Fetch image information
250 try:
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")
255 return
256
257 try:
258 pod_spec = make_pod_spec(
259 image_info,
260 self.model.config,
261 self.relation_state,
262 self.model.app.name,
263 self.port,
264 )
265 except ValidationError as exc:
266 logger.exception("Config/Relation data validation error")
267 self.unit.status = BlockedStatus(str(exc))
268 return
269
270 if self.state.pod_spec != pod_spec:
271 self.model.pod.set_spec(pod_spec)
272 self.state.pod_spec = pod_spec
273
274 self.unit.status = ActiveStatus("ready")
275
276
277 if __name__ == "__main__":
278 main(LcmCharm)