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