Revert "Integrate MON and Prometheus"
[osm/devops.git] / installers / charm / osm-mon / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2022 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 # Learn more at: https://juju.is/docs/sdk
24
25 """OSM MON charm.
26
27 See more: https://charmhub.io/osm
28 """
29
30 import logging
31 from typing import Any, Dict
32 from urllib.parse import urlparse
33
34 from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
35 from charms.kafka_k8s.v0.kafka import KafkaRequires, _KafkaAvailableEvent
36 from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
37 from charms.osm_libs.v0.utils import (
38 CharmError,
39 DebugMode,
40 HostPath,
41 check_container_ready,
42 check_service_active,
43 )
44 from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
45 from lightkube.models.core_v1 import ServicePort
46 from ops.charm import ActionEvent, CharmBase, CharmEvents
47 from ops.framework import EventSource, StoredState
48 from ops.main import main
49 from ops.model import ActiveStatus, Container
50
51 from grafana_datasource_handler import (
52 DatasourceConfig,
53 GrafanaConfig,
54 GrafanaDataSourceHandler,
55 )
56 from legacy_interfaces import KeystoneClient, PrometheusClient
57
58 HOSTPATHS = [
59 HostPath(
60 config="mon-hostpath",
61 container_path="/usr/lib/python3/dist-packages/osm_mon",
62 ),
63 HostPath(
64 config="common-hostpath",
65 container_path="/usr/lib/python3/dist-packages/osm_common",
66 ),
67 HostPath(
68 config="n2vc-hostpath",
69 container_path="/usr/lib/python3/dist-packages/n2vc",
70 ),
71 ]
72 SERVICE_PORT = 8000
73
74 logger = logging.getLogger(__name__)
75
76
77 class MonEvents(CharmEvents):
78 """MON events."""
79
80 vca_data_changed = EventSource(VcaDataChangedEvent)
81 kafka_available = EventSource(_KafkaAvailableEvent)
82
83
84 class OsmMonCharm(CharmBase):
85 """OSM MON Kubernetes sidecar charm."""
86
87 on = MonEvents()
88 _stored = StoredState()
89 container_name = "mon"
90 service_name = "mon"
91
92 def __init__(self, *args):
93 super().__init__(*args)
94 self.kafka = KafkaRequires(self)
95 self.mongodb_client = DatabaseRequires(self, "mongodb", database_name="osm")
96 self.prometheus_client = PrometheusClient(self, "prometheus")
97 self.keystone_client = KeystoneClient(self, "keystone")
98 self.vca = VcaRequires(self)
99 self._observe_charm_events()
100 self.container: Container = self.unit.get_container(self.container_name)
101 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
102 self._patch_k8s_service()
103
104 @property
105 def external_hostname(self) -> str:
106 """External hostname property.
107
108 Returns:
109 str: the external hostname from config.
110 If not set, return the ClusterIP service name.
111 """
112 return self.config.get("external-hostname") or self.app.name
113
114 # ---------------------------------------------------------------------------
115 # Handlers for Charm Events
116 # ---------------------------------------------------------------------------
117
118 def _on_config_changed(self, _) -> None:
119 """Handler for the config-changed event."""
120 try:
121 self._validate_config()
122 self._check_relations()
123 # Check if the container is ready.
124 # Eventually it will become ready after the first pebble-ready event.
125 check_container_ready(self.container)
126 if not self.debug_mode.started:
127 self._configure_service(self.container)
128 # Update charm status
129 self._on_update_status()
130 except CharmError as e:
131 logger.debug(e.message)
132 self.unit.status = e.status
133
134 def _on_update_status(self, _=None) -> None:
135 """Handler for the update-status event."""
136 try:
137 self._validate_config()
138 self._check_relations()
139 check_container_ready(self.container)
140 if self.debug_mode.started:
141 return
142 check_service_active(self.container, self.service_name)
143 self.unit.status = ActiveStatus()
144 except CharmError as e:
145 logger.debug(e.message)
146 self.unit.status = e.status
147
148 def _on_required_relation_broken(self, _) -> None:
149 """Handler for the kafka-broken event."""
150 try:
151 check_container_ready(self.container)
152 check_service_active(self.container, self.service_name)
153 self.container.stop(self.container_name)
154 except CharmError:
155 pass
156 self._on_update_status()
157
158 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
159 """Handler for the get-debug-mode-information action event."""
160 if not self.debug_mode.started:
161 event.fail("debug-mode has not started. Hint: juju config mon debug-mode=true")
162 return
163
164 debug_info = {
165 "command": self.debug_mode.command,
166 "password": self.debug_mode.password,
167 }
168 event.set_results(debug_info)
169
170 def _on_create_datasource_action(self, event: ActionEvent) -> None:
171 """Handler for the create-datasource action event."""
172 url = event.params["url"]
173 if not self._is_valid_url(url):
174 event.fail(f"Invalid datasource url '{url}'")
175 return
176 grafana_config = self._get_grafana_config()
177 datasource_config = DatasourceConfig(event.params["name"], url)
178 response = GrafanaDataSourceHandler.create_datasource(grafana_config, datasource_config)
179 logger.debug(response)
180 if response.is_success:
181 event.set_results(response.results)
182 return
183 event.fail(response.message)
184
185 def _on_list_datasources_action(self, event: ActionEvent) -> None:
186 """Handler for the list-datasource action event."""
187 grafana_config = self._get_grafana_config()
188 response = GrafanaDataSourceHandler.list_datasources(grafana_config)
189 logger.debug(response)
190 if response.is_success:
191 event.set_results(response.results)
192 return
193 event.fail(response.message)
194
195 def _on_delete_datasource_action(self, event: ActionEvent) -> None:
196 """Handler for the delete-datasource action event."""
197 datasource_name = event.params["name"]
198 grafana_config = self._get_grafana_config()
199 response = GrafanaDataSourceHandler.delete_datasource(grafana_config, datasource_name)
200 logger.debug(response)
201 if not response.is_success:
202 event.fail(response.message)
203
204 def _get_grafana_config(self) -> GrafanaConfig:
205 return GrafanaConfig(
206 self.config.get("grafana-user", ""),
207 self.config.get("grafana-password", ""),
208 self.config.get("grafana-url", ""),
209 )
210
211 # ---------------------------------------------------------------------------
212 # Validation and configuration and more
213 # ---------------------------------------------------------------------------
214
215 def _observe_charm_events(self) -> None:
216 event_handler_mapping = {
217 # Core lifecycle events
218 self.on.mon_pebble_ready: self._on_config_changed,
219 self.on.config_changed: self._on_config_changed,
220 self.on.update_status: self._on_update_status,
221 # Relation events
222 self.on.vca_data_changed: self._on_config_changed,
223 self.on.kafka_available: self._on_config_changed,
224 self.on["kafka"].relation_broken: self._on_required_relation_broken,
225 self.mongodb_client.on.database_created: self._on_config_changed,
226 self.on["mongodb"].relation_broken: self._on_required_relation_broken,
227 # Action events
228 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
229 self.on.create_datasource_action: self._on_create_datasource_action,
230 self.on.list_datasources_action: self._on_list_datasources_action,
231 self.on.delete_datasource_action: self._on_delete_datasource_action,
232 }
233 for relation in [self.on[rel_name] for rel_name in ["prometheus", "keystone"]]:
234 event_handler_mapping[relation.relation_changed] = self._on_config_changed
235 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
236
237 for event, handler in event_handler_mapping.items():
238 self.framework.observe(event, handler)
239
240 def _is_database_available(self) -> bool:
241 try:
242 return self.mongodb_client.is_resource_created()
243 except KeyError:
244 return False
245
246 def _validate_config(self) -> None:
247 """Validate charm configuration.
248
249 Raises:
250 CharmError: if charm configuration is invalid.
251 """
252 logger.debug("validating charm config")
253 missing_configs = []
254 grafana_configs = ["grafana-url", "grafana-user", "grafana-password"]
255 for config in grafana_configs:
256 if not self.config.get(config):
257 missing_configs.append(config)
258
259 if missing_configs:
260 config_str = ", ".join(missing_configs)
261 error_msg = f"need {config_str} config"
262 logger.warning(error_msg)
263 raise CharmError(error_msg)
264
265 grafana_url = self.config["grafana-url"]
266 if not self._is_valid_url(grafana_url):
267 raise CharmError(f"Invalid value for grafana-url config: '{grafana_url}'")
268
269 def _is_valid_url(self, url) -> bool:
270 return urlparse(url).hostname is not None
271
272 def _check_relations(self) -> None:
273 """Validate charm relations.
274
275 Raises:
276 CharmError: if charm configuration is invalid.
277 """
278 logger.debug("check for missing relations")
279 missing_relations = []
280
281 if not self.kafka.host or not self.kafka.port:
282 missing_relations.append("kafka")
283 if not self._is_database_available():
284 missing_relations.append("mongodb")
285 if self.prometheus_client.is_missing_data_in_app():
286 missing_relations.append("prometheus")
287 if self.keystone_client.is_missing_data_in_app():
288 missing_relations.append("keystone")
289
290 if missing_relations:
291 relations_str = ", ".join(missing_relations)
292 one_relation_missing = len(missing_relations) == 1
293 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
294 logger.warning(error_msg)
295 raise CharmError(error_msg)
296
297 def _configure_service(self, container: Container) -> None:
298 """Add Pebble layer with the mon service."""
299 logger.debug(f"configuring {self.app.name} service")
300 container.add_layer("mon", self._get_layer(), combine=True)
301 container.replan()
302
303 def _get_layer(self) -> Dict[str, Any]:
304 """Get layer for Pebble."""
305 environment = {
306 # General configuration
307 "OSMMON_GLOBAL_LOGLEVEL": self.config["log-level"],
308 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": self.config["openstack-default-granularity"],
309 "OSMMON_GLOBAL_REQUEST_TIMEOUT": self.config["global-request-timeout"],
310 "OSMMON_COLLECTOR_INTERVAL": self.config["collector-interval"],
311 "OSMMON_EVALUATOR_INTERVAL": self.config["evaluator-interval"],
312 "OSMMON_COLLECTOR_VM_INFRA_METRICS": self.config["vm-infra-metrics"],
313 # Kafka configuration
314 "OSMMON_MESSAGE_DRIVER": "kafka",
315 "OSMMON_MESSAGE_HOST": self.kafka.host,
316 "OSMMON_MESSAGE_PORT": self.kafka.port,
317 # Database configuration
318 "OSMMON_DATABASE_DRIVER": "mongo",
319 "OSMMON_DATABASE_URI": self._get_mongodb_uri(),
320 "OSMMON_DATABASE_COMMONKEY": self.config["database-commonkey"],
321 # Prometheus/grafana configuration
322 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
323 "OSMMON_PROMETHEUS_USER": self.prometheus_client.user,
324 "OSMMON_PROMETHEUS_PASSWORD": self.prometheus_client.password,
325 "OSMMON_GRAFANA_URL": self.config.get("grafana-url", ""),
326 "OSMMON_GRAFANA_USER": self.config.get("grafana-user", ""),
327 "OSMMON_GRAFANA_PASSWORD": self.config.get("grafana-password", ""),
328 "OSMMON_KEYSTONE_ENABLED": self.config["keystone-enabled"],
329 "OSMMON_KEYSTONE_URL": self.keystone_client.host,
330 "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
331 "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
332 "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
333 "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
334 "OSMMON_KEYSTONE_SERVICE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
335 }
336 logger.info(f"{environment}")
337 if self.vca.data:
338 environment["OSMMON_VCA_HOST"] = self.vca.data.endpoints
339 environment["OSMMON_VCA_SECRET"] = self.vca.data.secret
340 environment["OSMMON_VCA_USER"] = self.vca.data.user
341 environment["OSMMON_VCA_CACERT"] = self.vca.data.cacert
342 return {
343 "summary": "mon layer",
344 "description": "pebble config layer for mon",
345 "services": {
346 self.service_name: {
347 "override": "replace",
348 "summary": "mon service",
349 "command": "/bin/bash -c 'cd /app/osm_mon/ && /bin/bash start.sh'",
350 "startup": "enabled",
351 "user": "appuser",
352 "group": "appuser",
353 "working-dir": "/app/osm_mon", # This parameter has no effect in Juju 2.9.x
354 "environment": environment,
355 }
356 },
357 }
358
359 def _get_mongodb_uri(self):
360 return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
361
362 def _patch_k8s_service(self) -> None:
363 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
364 self.service_patcher = KubernetesServicePatch(self, [port])
365
366
367 if __name__ == "__main__": # pragma: no cover
368 main(OsmMonCharm)