blob: 52b6964a80ac72ba0a02c0194fcf81a209f633c0 [file] [log] [blame]
sousaeduabe73212020-11-04 15:13:35 +00001#!/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
23import logging
sousaeduabe73212020-11-04 15:13:35 +000024from typing import Any, Dict, NoReturn
25
26from ops.charm import CharmBase, CharmEvents
27from ops.framework import EventBase, EventSource, StoredState
28from ops.main import main
29from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
30from oci_image import OCIImageResource, OCIImageResourceError
31
32from pod_spec import make_pod_spec
33
34logger = logging.getLogger(__name__)
35
36LCM_PORT = 9999
37
38
39class ConfigurePodEvent(EventBase):
40 """Configure Pod event"""
41
42 pass
43
44
45class LcmEvents(CharmEvents):
46 """LCM Events"""
47
48 configure_pod = EventSource(ConfigurePodEvent)
49
50
51class 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 """
sousaedu13722ca2021-01-05 14:25:19 +0000114 message_host = event.relation.data[event.unit].get("host")
115 message_port = event.relation.data[event.unit].get("port")
sousaeduabe73212020-11-04 15:13:35 +0000116
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 """
sousaedu13722ca2021-01-05 14:25:19 +0000145 database_uri = event.relation.data[event.unit].get("connection_string")
sousaeduabe73212020-11-04 15:13:35 +0000146
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 """
sousaedu13722ca2021-01-05 14:25:19 +0000166 ro_host = event.relation.data[event.unit].get("host")
167 ro_port = event.relation.data[event.unit].get("port")
sousaeduabe73212020-11-04 15:13:35 +0000168
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 )
sousaedu13722ca2021-01-05 14:25:19 +0000258 except ValueError as exc:
sousaeduabe73212020-11-04 15:13:35 +0000259 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
270if __name__ == "__main__":
271 main(LcmCharm)