Refactoring MON Charm to use Operator Framework
[osm/devops.git] / installers / charm / mon / 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 MON_PORT = 8000
38
39
40 class ConfigurePodEvent(EventBase):
41 """Configure Pod event"""
42
43 pass
44
45
46 class MonEvents(CharmEvents):
47 """MON Events"""
48
49 configure_pod = EventSource(ConfigurePodEvent)
50
51
52 class MonCharm(CharmBase):
53 """MON Charm."""
54
55 state = StoredState()
56 on = MonEvents()
57
58 def __init__(self, *args) -> NoReturn:
59 """MON 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 # Prometheus data initialization
73 self.state.set_default(prometheus_host=None)
74 self.state.set_default(prometheus_port=None)
75
76 self.port = MON_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.prometheus_relation_changed, self._on_prometheus_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.prometheus_relation_departed, self._on_prometheus_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 """Clear kafka relation data.
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 """Clear mongodb relation data.
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_prometheus_relation_changed(self, event: EventBase) -> NoReturn:
166 """Reads information about the prometheus relation.
167
168 Args:
169 event (EventBase): Prometheus relation event.
170 """
171 data_loc = event.unit if event.unit else event.app
172
173 prometheus_host = event.relation.data[data_loc].get("hostname")
174 prometheus_port = event.relation.data[data_loc].get("port")
175
176 if (
177 prometheus_host
178 and prometheus_port
179 and (
180 self.state.prometheus_host != prometheus_host
181 or self.state.prometheus_port != prometheus_port
182 )
183 ):
184 self.state.prometheus_host = prometheus_host
185 self.state.prometheus_port = prometheus_port
186 self.on.configure_pod.emit()
187
188 def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn:
189 """Clear prometheus relation data.
190
191 Args:
192 event (EventBase): Prometheus relation event.
193 """
194 self.state.prometheus_host = None
195 self.state.prometheus_port = None
196 self.on.configure_pod.emit()
197
198 def _missing_relations(self) -> str:
199 """Checks if there missing relations.
200
201 Returns:
202 str: string with missing relations
203 """
204 data_status = {
205 "kafka": self.state.message_host,
206 "mongodb": self.state.database_uri,
207 "prometheus": self.state.prometheus_host,
208 }
209
210 missing_relations = [k for k, v in data_status.items() if not v]
211
212 return ", ".join(missing_relations)
213
214 @property
215 def relation_state(self) -> Dict[str, Any]:
216 """Collects relation state configuration for pod spec assembly.
217
218 Returns:
219 Dict[str, Any]: relation state information.
220 """
221 relation_state = {
222 "message_host": self.state.message_host,
223 "message_port": self.state.message_port,
224 "database_uri": self.state.database_uri,
225 "prometheus_host": self.state.prometheus_host,
226 "prometheus_port": self.state.prometheus_port,
227 }
228
229 return relation_state
230
231 def configure_pod(self, event: EventBase) -> NoReturn:
232 """Assemble the pod spec and apply it, if possible.
233
234 Args:
235 event (EventBase): Hook or Relation event that started the
236 function.
237 """
238 if missing := self._missing_relations():
239 self.unit.status = BlockedStatus(
240 "Waiting for {0} relation{1}".format(
241 missing, "s" if "," in missing else ""
242 )
243 )
244 return
245
246 if not self.unit.is_leader():
247 self.unit.status = ActiveStatus("ready")
248 return
249
250 self.unit.status = MaintenanceStatus("Assembling pod spec")
251
252 # Fetch image information
253 try:
254 self.unit.status = MaintenanceStatus("Fetching image information")
255 image_info = self.image.fetch()
256 except OCIImageResourceError:
257 self.unit.status = BlockedStatus("Error fetching image information")
258 return
259
260 try:
261 pod_spec = make_pod_spec(
262 image_info,
263 self.model.config,
264 self.relation_state,
265 self.model.app.name,
266 self.port,
267 )
268 except ValidationError as exc:
269 LOGGER.exception("Config/Relation data validation error")
270 self.unit.status = BlockedStatus(str(exc))
271 return
272
273 if self.state.pod_spec != pod_spec:
274 self.model.pod.set_spec(pod_spec)
275 self.state.pod_spec = pod_spec
276
277 self.unit.status = ActiveStatus("ready")
278
279
280 if __name__ == "__main__":
281 main(MonCharm)