4371d47ab2cca53058b80a7d1fc0eb2160cdb9a7
[osm/devops.git] / installers / charm / prometheus / src / charm.py
1 #!/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
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 PROMETHEUS_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 PrometheusCharm(CharmBase):
96 """Prometheus Charm."""
97
98 state = StoredState()
99
100 def __init__(self, *args) -> NoReturn:
101 """Prometheus Charm constructor."""
102 super().__init__(*args)
103
104 # Internal state initialization
105 self.state.set_default(pod_spec=None)
106
107 self.port = PROMETHEUS_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 provided relation events
115 self.framework.observe(
116 self.on.prometheus_relation_joined, self._publish_prometheus_info
117 )
118
119 def _publish_prometheus_info(self, event: EventBase) -> NoReturn:
120 """Publishes Prometheus information.
121
122 Args:
123 event (EventBase): Prometheus relation event.
124 """
125 if self.unit.is_leader():
126 rel_data = {
127 "host": self.model.app.name,
128 "port": str(PROMETHEUS_PORT),
129 }
130 for k, v in rel_data.items():
131 event.relation.data[self.app][k] = v
132
133 @property
134 def relations_requirements(self):
135 return []
136
137 def get_relation_state(self):
138 relation_state = {}
139 for relation_requirements in self.relations_requirements:
140 data = get_relation_data(self, relation_requirements)
141 relation_state = {**relation_state, **data}
142 check_missing_relation_data(relation_state, self.relations_requirements)
143 return relation_state
144
145 def configure_pod(self, _=None) -> NoReturn:
146 """Assemble the pod spec and apply it, if possible.
147
148 Args:
149 event (EventBase): Hook or Relation event that started the
150 function.
151 """
152 if not self.unit.is_leader():
153 self.unit.status = ActiveStatus("ready")
154 return
155
156 relation_state = None
157 try:
158 relation_state = self.get_relation_state()
159 except RelationsMissing as exc:
160 logger.exception("Relation missing error")
161 self.unit.status = BlockedStatus(exc.message)
162 return
163
164 self.unit.status = MaintenanceStatus("Assembling pod spec")
165
166 # Fetch image information
167 try:
168 self.unit.status = MaintenanceStatus("Fetching image information")
169 image_info = self.image.fetch()
170 except OCIImageResourceError:
171 self.unit.status = BlockedStatus("Error fetching image information")
172 return
173
174 try:
175 pod_spec = make_pod_spec(
176 image_info,
177 self.model.config,
178 relation_state,
179 self.model.app.name,
180 self.port,
181 )
182 except ValueError as exc:
183 logger.exception("Config/Relation data validation error")
184 self.unit.status = BlockedStatus(str(exc))
185 return
186
187 if self.state.pod_spec != pod_spec:
188 self.model.pod.set_spec(pod_spec)
189 self.state.pod_spec = pod_spec
190
191 self.unit.status = ActiveStatus("ready")
192
193
194 if __name__ == "__main__":
195 main(PrometheusCharm)