blob: 02a600c8f70516febbb615514f99c25a8ac0a3c6 [file] [log] [blame]
sousaedu903379c2021-02-08 13:34:21 +01001#!/usr/bin/env python3
2# Copyright 2021 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
24from pathlib import Path
25from typing import Dict, List, NoReturn
sousaedu3884e232021-02-25 21:32:25 +010026from urllib.parse import urlparse
sousaedu903379c2021-02-08 13:34:21 +010027
28from ops.charm import CharmBase
29from ops.framework import EventBase, StoredState
30from ops.main import main
31from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
32from oci_image import OCIImageResource, OCIImageResourceError
33
34from pod_spec import make_pod_spec
35
36logger = logging.getLogger(__name__)
37
sousaedu3884e232021-02-25 21:32:25 +010038MONGODB_EXPORTER_PORT = 9216
sousaedu903379c2021-02-08 13:34:21 +010039
40
41class RelationsMissing(Exception):
42 def __init__(self, missing_relations: List):
43 self.message = ""
44 if missing_relations and isinstance(missing_relations, list):
45 self.message += f'Waiting for {", ".join(missing_relations)} relation'
46 if "," in self.message:
47 self.message += "s"
48
49
50class RelationDefinition:
51 def __init__(self, relation_name: str, keys: List, source_type):
52 if source_type != Application and source_type != Unit:
53 raise TypeError(
54 "source_type should be ops.model.Application or ops.model.Unit"
55 )
56 self.relation_name = relation_name
57 self.keys = keys
58 self.source_type = source_type
59
60
61def check_missing_relation_data(
62 data: Dict,
63 expected_relations_data: List[RelationDefinition],
64):
65 missing_relations = []
66 for relation_data in expected_relations_data:
67 if not all(
68 f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
69 ):
70 missing_relations.append(relation_data.relation_name)
71 if missing_relations:
72 raise RelationsMissing(missing_relations)
73
74
75def get_relation_data(
76 charm: CharmBase,
77 relation_data: RelationDefinition,
78) -> Dict:
79 data = {}
80 relation = charm.model.get_relation(relation_data.relation_name)
81 if relation:
82 self_app_unit = (
83 charm.app if relation_data.source_type == Application else charm.unit
84 )
85 expected_type = relation_data.source_type
86 for app_unit in relation.data:
87 if app_unit != self_app_unit and isinstance(app_unit, expected_type):
88 if all(k in relation.data[app_unit] for k in relation_data.keys):
89 for k in relation_data.keys:
90 data[f"{relation_data.relation_name}_{k}"] = relation.data[
91 app_unit
92 ].get(k)
93 break
94 return data
95
96
sousaedu3884e232021-02-25 21:32:25 +010097class MongodbExporterCharm(CharmBase):
98 """Mongodb Exporter Charm."""
sousaedu903379c2021-02-08 13:34:21 +010099
100 state = StoredState()
101
102 def __init__(self, *args) -> NoReturn:
sousaedu3884e232021-02-25 21:32:25 +0100103 """Mongodb Exporter Charm constructor."""
sousaedu903379c2021-02-08 13:34:21 +0100104 super().__init__(*args)
105
106 # Internal state initialization
107 self.state.set_default(pod_spec=None)
108
sousaedu3884e232021-02-25 21:32:25 +0100109 self.port = MONGODB_EXPORTER_PORT
sousaedu903379c2021-02-08 13:34:21 +0100110 self.image = OCIImageResource(self, "image")
111
112 # Registering regular events
113 self.framework.observe(self.on.start, self.configure_pod)
114 self.framework.observe(self.on.config_changed, self.configure_pod)
115
116 # Registering required relation events
117 self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod)
118
119 # Registering required relation departed events
120 self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod)
121
122 # Registering provided relation events
123 self.framework.observe(
sousaedu3884e232021-02-25 21:32:25 +0100124 self.on.prometheus_scrape_relation_joined, self._publish_scrape_info
sousaedu903379c2021-02-08 13:34:21 +0100125 )
126 self.framework.observe(
127 self.on.grafana_dashboard_relation_joined, self._publish_dashboard_info
128 )
129
sousaedu3884e232021-02-25 21:32:25 +0100130 def _publish_scrape_info(self, event: EventBase) -> NoReturn:
131 """Publishes scrape information.
sousaedu903379c2021-02-08 13:34:21 +0100132
133 Args:
134 event (EventBase): Exporter relation event.
135 """
136 rel_data = {
sousaedu3884e232021-02-25 21:32:25 +0100137 "hostname": urlparse(self.model.config["site_url"]).hostname
138 if self.model.config["site_url"]
139 else self.model.app.name,
140 "port": "80"
141 if self.model.config["site_url"]
142 else str(MONGODB_EXPORTER_PORT),
143 "metrics_path": "/metrics",
144 "scrape_interval": "30s",
145 "scrape_timeout": "15s",
sousaedu903379c2021-02-08 13:34:21 +0100146 }
147 for k, v in rel_data.items():
148 event.relation.data[self.unit][k] = v
149
150 def _publish_dashboard_info(self, event: EventBase) -> NoReturn:
151 """Publishes dashboard information.
152
153 Args:
154 event (EventBase): Exporter relation event.
155 """
156 rel_data = {
sousaedu3884e232021-02-25 21:32:25 +0100157 "name": "osm-mongodb",
158 "dashboard": Path("files/mongodb_exporter_dashboard.json").read_text(),
sousaedu903379c2021-02-08 13:34:21 +0100159 }
160 for k, v in rel_data.items():
161 event.relation.data[self.unit][k] = v
162
163 @property
164 def relations_requirements(self):
165 return [RelationDefinition("mongodb", ["connection_string"], Unit)]
166
167 def get_relation_state(self):
168 relation_state = {}
169 for relation_requirements in self.relations_requirements:
170 data = get_relation_data(self, relation_requirements)
171 relation_state = {**relation_state, **data}
172 check_missing_relation_data(relation_state, self.relations_requirements)
173 return relation_state
174
175 def configure_pod(self, _=None) -> NoReturn:
176 """Assemble the pod spec and apply it, if possible.
177
178 Args:
179 event (EventBase): Hook or Relation event that started the
180 function.
181 """
182 if not self.unit.is_leader():
183 self.unit.status = ActiveStatus("ready")
184 return
185
186 relation_state = None
187 try:
188 relation_state = self.get_relation_state()
189 except RelationsMissing as exc:
190 logger.exception("Relation missing error")
191 self.unit.status = BlockedStatus(exc.message)
192 return
193
194 self.unit.status = MaintenanceStatus("Assembling pod spec")
195
196 # Fetch image information
197 try:
198 self.unit.status = MaintenanceStatus("Fetching image information")
199 image_info = self.image.fetch()
200 except OCIImageResourceError:
201 self.unit.status = BlockedStatus("Error fetching image information")
202 return
203
204 try:
205 pod_spec = make_pod_spec(
206 image_info,
207 self.model.config,
208 relation_state,
209 self.model.app.name,
210 self.port,
211 )
212 except ValueError as exc:
213 logger.exception("Config/Relation data validation error")
214 self.unit.status = BlockedStatus(str(exc))
215 return
216
217 if self.state.pod_spec != pod_spec:
218 self.model.pod.set_spec(pod_spec)
219 self.state.pod_spec = pod_spec
220
221 self.unit.status = ActiveStatus("ready")
222
223
224if __name__ == "__main__":
sousaedu3884e232021-02-25 21:32:25 +0100225 main(MongodbExporterCharm)