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