8e6d5764e40175c4ffb3e617cc39ff4117bc6a70
[osm/devops.git] / installers / charm / ro / 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 Dict, List, NoReturn
25
26 from ops.charm import CharmBase
27 from ops.framework import EventBase, StoredState
28 from ops.main import main
29 from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
30 from oci_image import OCIImageResource, OCIImageResourceError
31
32 from pod_spec import make_pod_spec
33
34 logger = logging.getLogger(__name__)
35
36 RO_PORT = 9090
37
38
39 class RelationsMissing(Exception):
40 def __init__(self, missing_relations: List):
41 self.message = ""
42 if missing_relations and isinstance(missing_relations, list):
43 self.message += f'Waiting for {", ".join(missing_relations)} relation'
44 if "," in self.message:
45 self.message += "s"
46
47
48 class RelationDefinition:
49 def __init__(self, relation_name: str, keys: List, source_type):
50 if source_type != Application and source_type != Unit:
51 raise TypeError(
52 "source_type should be ops.model.Application or ops.model.Unit"
53 )
54 self.relation_name = relation_name
55 self.keys = keys
56 self.source_type = source_type
57
58
59 def check_missing_relation_data(
60 data: Dict,
61 expected_relations_data: List[RelationDefinition],
62 ):
63 missing_relations = []
64 for relation_data in expected_relations_data:
65 if not all(
66 f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
67 ):
68 missing_relations.append(relation_data.relation_name)
69 if missing_relations:
70 raise RelationsMissing(missing_relations)
71
72
73 def get_relation_data(
74 charm: CharmBase,
75 relation_data: RelationDefinition,
76 ) -> Dict:
77 data = {}
78 relation = charm.model.get_relation(relation_data.relation_name)
79 if relation:
80 self_app_unit = (
81 charm.app if relation_data.source_type == Application else charm.unit
82 )
83 expected_type = relation_data.source_type
84 for app_unit in relation.data:
85 if app_unit != self_app_unit and isinstance(app_unit, expected_type):
86 if all(k in relation.data[app_unit] for k in relation_data.keys):
87 for k in relation_data.keys:
88 data[f"{relation_data.relation_name}_{k}"] = relation.data[
89 app_unit
90 ].get(k)
91 break
92 return data
93
94
95 class RoCharm(CharmBase):
96 """RO Charm."""
97
98 state = StoredState()
99
100 def __init__(self, *args) -> NoReturn:
101 """RO Charm constructor."""
102 super().__init__(*args)
103
104 # Internal state initialization
105 self.state.set_default(pod_spec=None)
106
107 self.port = RO_PORT
108 self.image = OCIImageResource(self, "image")
109
110 # Registering regular events
111 self.framework.observe(self.on.start, self.configure_pod)
112 self.framework.observe(self.on.config_changed, self.configure_pod)
113
114 # Registering required relation events
115 self.framework.observe(self.on.kafka_relation_changed, self.configure_pod)
116 self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod)
117 self.framework.observe(self.on.mysql_relation_changed, self.configure_pod)
118
119 # Registering required relation departed events
120 self.framework.observe(self.on.kafka_relation_departed, self.configure_pod)
121 self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod)
122 self.framework.observe(self.on.mysql_relation_departed, self.configure_pod)
123
124 # Registering required relation broken events
125 self.framework.observe(self.on.kafka_relation_broken, self.configure_pod)
126 self.framework.observe(self.on.mongodb_relation_broken, self.configure_pod)
127 self.framework.observe(self.on.mysql_relation_broken, self.configure_pod)
128
129 # Registering provided relation events
130 self.framework.observe(self.on.ro_relation_joined, self._publish_ro_info)
131
132 def _publish_ro_info(self, event: EventBase) -> NoReturn:
133 """Publishes RO information.
134
135 Args:
136 event (EventBase): RO relation event.
137 """
138 if self.unit.is_leader():
139 rel_data = {
140 "host": self.model.app.name,
141 "port": str(RO_PORT),
142 }
143 for k, v in rel_data.items():
144 event.relation.data[self.app][k] = v
145
146 @property
147 def relations_requirements(self):
148 if self.model.config["enable_ng_ro"]:
149 return [
150 RelationDefinition("kafka", ["host", "port"], Unit),
151 RelationDefinition("mongodb", ["connection_string"], Unit),
152 ]
153 else:
154 return [
155 RelationDefinition(
156 "mysql", ["host", "port", "user", "password", "root_password"], Unit
157 )
158 ]
159
160 def get_relation_state(self):
161 relation_state = {}
162 for relation_requirements in self.relations_requirements:
163 data = get_relation_data(self, relation_requirements)
164 relation_state = {**relation_state, **data}
165 check_missing_relation_data(relation_state, self.relations_requirements)
166 return relation_state
167
168 def configure_pod(self, _=None) -> NoReturn:
169 """Assemble the pod spec and apply it, if possible.
170
171 Args:
172 event (EventBase): Hook or Relation event that started the
173 function.
174 """
175 if not self.unit.is_leader():
176 self.unit.status = ActiveStatus("ready")
177 return
178
179 relation_state = None
180 try:
181 relation_state = self.get_relation_state()
182 except RelationsMissing as exc:
183 logger.exception("Relation missing error")
184 self.unit.status = BlockedStatus(exc.message)
185 return
186
187 self.unit.status = MaintenanceStatus("Assembling pod spec")
188
189 # Fetch image information
190 try:
191 self.unit.status = MaintenanceStatus("Fetching image information")
192 image_info = self.image.fetch()
193 except OCIImageResourceError:
194 self.unit.status = BlockedStatus("Error fetching image information")
195 return
196
197 try:
198 pod_spec = make_pod_spec(
199 image_info,
200 self.model.config,
201 relation_state,
202 self.model.app.name,
203 self.port,
204 )
205 except ValueError as exc:
206 logger.exception("Config/Relation data validation error")
207 self.unit.status = BlockedStatus(str(exc))
208 return
209
210 if self.state.pod_spec != pod_spec:
211 self.model.pod.set_spec(pod_spec)
212 self.state.pod_spec = pod_spec
213
214 self.unit.status = ActiveStatus("ready")
215
216
217 if __name__ == "__main__":
218 main(RoCharm)