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