blob: bb98ccf057ad8b53fca0426f13039b076159b0f9 [file] [log] [blame]
beierlm8ea1f372022-06-30 09:02:30 -04001#!/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
Dario Faccin56db64f2023-02-16 20:50:38 +010025"""OSM MON charm.
beierlm8ea1f372022-06-30 09:02:30 -040026
27See more: https://charmhub.io/osm
28"""
29
30import logging
31from typing import Any, Dict
Patricia Reinoso87b620a2023-06-20 15:23:47 +000032from urllib.parse import urlparse
beierlm8ea1f372022-06-30 09:02:30 -040033
Dario Faccin56db64f2023-02-16 20:50:38 +010034from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
beierlm8ea1f372022-06-30 09:02:30 -040035from charms.kafka_k8s.v0.kafka import KafkaRequires, _KafkaAvailableEvent
36from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
37from charms.osm_libs.v0.utils import (
38 CharmError,
39 DebugMode,
40 HostPath,
41 check_container_ready,
42 check_service_active,
43)
44from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
Patricia Reinosoc5b62062023-06-26 16:31:17 +000045from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider
beierlm8ea1f372022-06-30 09:02:30 -040046from lightkube.models.core_v1 import ServicePort
47from ops.charm import ActionEvent, CharmBase, CharmEvents
48from ops.framework import EventSource, StoredState
49from ops.main import main
50from ops.model import ActiveStatus, Container
51
Patricia Reinoso87b620a2023-06-20 15:23:47 +000052from grafana_datasource_handler import (
53 DatasourceConfig,
54 GrafanaConfig,
55 GrafanaDataSourceHandler,
56)
Patricia Reinosoc5b62062023-06-26 16:31:17 +000057from legacy_interfaces import KeystoneClient
beierlm8ea1f372022-06-30 09:02:30 -040058
59HOSTPATHS = [
60 HostPath(
61 config="mon-hostpath",
62 container_path="/usr/lib/python3/dist-packages/osm_mon",
63 ),
64 HostPath(
65 config="common-hostpath",
66 container_path="/usr/lib/python3/dist-packages/osm_common",
67 ),
68 HostPath(
69 config="n2vc-hostpath",
70 container_path="/usr/lib/python3/dist-packages/n2vc",
71 ),
72]
73SERVICE_PORT = 8000
Patricia Reinosoc5b62062023-06-26 16:31:17 +000074PROMETHEUS_RELATION = "metrics-endpoint"
beierlm8ea1f372022-06-30 09:02:30 -040075
76logger = logging.getLogger(__name__)
77
78
79class MonEvents(CharmEvents):
80 """MON events."""
81
82 vca_data_changed = EventSource(VcaDataChangedEvent)
83 kafka_available = EventSource(_KafkaAvailableEvent)
84
85
86class OsmMonCharm(CharmBase):
87 """OSM MON Kubernetes sidecar charm."""
88
89 on = MonEvents()
90 _stored = StoredState()
91 container_name = "mon"
92 service_name = "mon"
93
94 def __init__(self, *args):
95 super().__init__(*args)
96 self.kafka = KafkaRequires(self)
Dario Faccin56db64f2023-02-16 20:50:38 +010097 self.mongodb_client = DatabaseRequires(self, "mongodb", database_name="osm")
Patricia Reinosoc5b62062023-06-26 16:31:17 +000098 self._set_metrics_endpoint_provider()
beierlm8ea1f372022-06-30 09:02:30 -040099 self.keystone_client = KeystoneClient(self, "keystone")
100 self.vca = VcaRequires(self)
101 self._observe_charm_events()
102 self.container: Container = self.unit.get_container(self.container_name)
103 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
104 self._patch_k8s_service()
105
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000106 def _set_metrics_endpoint_provider(self):
107 prometheus_jobs = [
108 {
109 "job_name": "mon_exporter",
110 "static_configs": [{"targets": ["*:{}".format(SERVICE_PORT)]}],
111 }
112 ]
113 refresh_events = [
114 self.on.mon_pebble_ready,
115 self.on.update_status,
116 ]
117 self.prometheus_metrics_endpoint = MetricsEndpointProvider(
118 self, jobs=prometheus_jobs, refresh_event=refresh_events
119 )
120
beierlm8ea1f372022-06-30 09:02:30 -0400121 @property
122 def external_hostname(self) -> str:
123 """External hostname property.
124
125 Returns:
126 str: the external hostname from config.
127 If not set, return the ClusterIP service name.
128 """
129 return self.config.get("external-hostname") or self.app.name
130
131 # ---------------------------------------------------------------------------
132 # Handlers for Charm Events
133 # ---------------------------------------------------------------------------
134
135 def _on_config_changed(self, _) -> None:
136 """Handler for the config-changed event."""
137 try:
138 self._validate_config()
139 self._check_relations()
140 # Check if the container is ready.
141 # Eventually it will become ready after the first pebble-ready event.
142 check_container_ready(self.container)
Guillermo Calvino7292e6e2022-08-18 11:48:30 +0200143 if not self.debug_mode.started:
144 self._configure_service(self.container)
beierlm8ea1f372022-06-30 09:02:30 -0400145 # Update charm status
146 self._on_update_status()
147 except CharmError as e:
148 logger.debug(e.message)
149 self.unit.status = e.status
150
151 def _on_update_status(self, _=None) -> None:
152 """Handler for the update-status event."""
153 try:
154 self._validate_config()
155 self._check_relations()
156 check_container_ready(self.container)
157 if self.debug_mode.started:
158 return
159 check_service_active(self.container, self.service_name)
160 self.unit.status = ActiveStatus()
161 except CharmError as e:
162 logger.debug(e.message)
163 self.unit.status = e.status
164
165 def _on_required_relation_broken(self, _) -> None:
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000166 """Handler for the relation-broken event."""
beierlm8ea1f372022-06-30 09:02:30 -0400167 try:
168 check_container_ready(self.container)
169 check_service_active(self.container, self.service_name)
170 self.container.stop(self.container_name)
171 except CharmError:
172 pass
173 self._on_update_status()
174
175 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
176 """Handler for the get-debug-mode-information action event."""
177 if not self.debug_mode.started:
Dario Faccin4734de12023-02-15 10:52:55 +0100178 event.fail("debug-mode has not started. Hint: juju config mon debug-mode=true")
beierlm8ea1f372022-06-30 09:02:30 -0400179 return
180
Guillermo Calvino569faee2022-11-23 10:31:18 +0100181 debug_info = {
182 "command": self.debug_mode.command,
183 "password": self.debug_mode.password,
184 }
beierlm8ea1f372022-06-30 09:02:30 -0400185 event.set_results(debug_info)
186
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000187 def _on_create_datasource_action(self, event: ActionEvent) -> None:
188 """Handler for the create-datasource action event."""
189 url = event.params["url"]
190 if not self._is_valid_url(url):
191 event.fail(f"Invalid datasource url '{url}'")
192 return
193 grafana_config = self._get_grafana_config()
194 datasource_config = DatasourceConfig(event.params["name"], url)
195 response = GrafanaDataSourceHandler.create_datasource(grafana_config, datasource_config)
196 logger.debug(response)
197 if response.is_success:
198 event.set_results(response.results)
199 return
200 event.fail(response.message)
201
202 def _on_list_datasources_action(self, event: ActionEvent) -> None:
203 """Handler for the list-datasource action event."""
204 grafana_config = self._get_grafana_config()
205 response = GrafanaDataSourceHandler.list_datasources(grafana_config)
206 logger.debug(response)
207 if response.is_success:
208 event.set_results(response.results)
209 return
210 event.fail(response.message)
211
212 def _on_delete_datasource_action(self, event: ActionEvent) -> None:
213 """Handler for the delete-datasource action event."""
214 datasource_name = event.params["name"]
215 grafana_config = self._get_grafana_config()
216 response = GrafanaDataSourceHandler.delete_datasource(grafana_config, datasource_name)
217 logger.debug(response)
218 if not response.is_success:
219 event.fail(response.message)
220
221 def _get_grafana_config(self) -> GrafanaConfig:
222 return GrafanaConfig(
223 self.config.get("grafana-user", ""),
224 self.config.get("grafana-password", ""),
225 self.config.get("grafana-url", ""),
226 )
227
beierlm8ea1f372022-06-30 09:02:30 -0400228 # ---------------------------------------------------------------------------
229 # Validation and configuration and more
230 # ---------------------------------------------------------------------------
231
232 def _observe_charm_events(self) -> None:
233 event_handler_mapping = {
234 # Core lifecycle events
235 self.on.mon_pebble_ready: self._on_config_changed,
236 self.on.config_changed: self._on_config_changed,
237 self.on.update_status: self._on_update_status,
238 # Relation events
239 self.on.vca_data_changed: self._on_config_changed,
240 self.on.kafka_available: self._on_config_changed,
241 self.on["kafka"].relation_broken: self._on_required_relation_broken,
Dario Faccin56db64f2023-02-16 20:50:38 +0100242 self.mongodb_client.on.database_created: self._on_config_changed,
243 self.on["mongodb"].relation_broken: self._on_required_relation_broken,
beierlm8ea1f372022-06-30 09:02:30 -0400244 # Action events
245 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000246 self.on.create_datasource_action: self._on_create_datasource_action,
247 self.on.list_datasources_action: self._on_list_datasources_action,
248 self.on.delete_datasource_action: self._on_delete_datasource_action,
beierlm8ea1f372022-06-30 09:02:30 -0400249 }
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000250
251 for relation in [self.on[rel_name] for rel_name in [PROMETHEUS_RELATION, "keystone"]]:
beierlm8ea1f372022-06-30 09:02:30 -0400252 event_handler_mapping[relation.relation_changed] = self._on_config_changed
Dario Faccin4734de12023-02-15 10:52:55 +0100253 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
beierlm8ea1f372022-06-30 09:02:30 -0400254
255 for event, handler in event_handler_mapping.items():
256 self.framework.observe(event, handler)
257
Dario Faccin56db64f2023-02-16 20:50:38 +0100258 def _is_database_available(self) -> bool:
259 try:
260 return self.mongodb_client.is_resource_created()
261 except KeyError:
262 return False
263
beierlm8ea1f372022-06-30 09:02:30 -0400264 def _validate_config(self) -> None:
265 """Validate charm configuration.
266
267 Raises:
268 CharmError: if charm configuration is invalid.
269 """
270 logger.debug("validating charm config")
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000271 self._validate_mandatory_config_is_set()
272 self._validate_urls_in_config()
273
274 def _validate_mandatory_config_is_set(self):
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000275 missing_configs = []
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000276 mandatory_configs = ["grafana-url", "grafana-user", "grafana-password", "prometheus-url"]
277 for config in mandatory_configs:
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000278 if not self.config.get(config):
279 missing_configs.append(config)
280
281 if missing_configs:
282 config_str = ", ".join(missing_configs)
283 error_msg = f"need {config_str} config"
284 logger.warning(error_msg)
285 raise CharmError(error_msg)
286
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000287 def _validate_urls_in_config(self):
288 urls_to_validate = ["grafana-url", "prometheus-url"]
289 for param in urls_to_validate:
290 url = self.config[param]
291 if not self._is_valid_url(url):
292 raise CharmError(f"Invalid value for {param} config: '{url}'")
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000293
294 def _is_valid_url(self, url) -> bool:
295 return urlparse(url).hostname is not None
beierlm8ea1f372022-06-30 09:02:30 -0400296
297 def _check_relations(self) -> None:
298 """Validate charm relations.
299
300 Raises:
301 CharmError: if charm configuration is invalid.
302 """
303 logger.debug("check for missing relations")
304 missing_relations = []
305
306 if not self.kafka.host or not self.kafka.port:
307 missing_relations.append("kafka")
Dario Faccin56db64f2023-02-16 20:50:38 +0100308 if not self._is_database_available():
beierlm8ea1f372022-06-30 09:02:30 -0400309 missing_relations.append("mongodb")
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000310 if not self.framework.model.get_relation(PROMETHEUS_RELATION):
beierlm8ea1f372022-06-30 09:02:30 -0400311 missing_relations.append("prometheus")
312 if self.keystone_client.is_missing_data_in_app():
313 missing_relations.append("keystone")
314
315 if missing_relations:
316 relations_str = ", ".join(missing_relations)
317 one_relation_missing = len(missing_relations) == 1
Dario Faccin4734de12023-02-15 10:52:55 +0100318 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
beierlm8ea1f372022-06-30 09:02:30 -0400319 logger.warning(error_msg)
320 raise CharmError(error_msg)
321
322 def _configure_service(self, container: Container) -> None:
323 """Add Pebble layer with the mon service."""
324 logger.debug(f"configuring {self.app.name} service")
325 container.add_layer("mon", self._get_layer(), combine=True)
326 container.replan()
327
328 def _get_layer(self) -> Dict[str, Any]:
329 """Get layer for Pebble."""
330 environment = {
331 # General configuration
332 "OSMMON_GLOBAL_LOGLEVEL": self.config["log-level"],
Dario Faccin4734de12023-02-15 10:52:55 +0100333 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": self.config["openstack-default-granularity"],
beierlm8ea1f372022-06-30 09:02:30 -0400334 "OSMMON_GLOBAL_REQUEST_TIMEOUT": self.config["global-request-timeout"],
335 "OSMMON_COLLECTOR_INTERVAL": self.config["collector-interval"],
336 "OSMMON_EVALUATOR_INTERVAL": self.config["evaluator-interval"],
Guillermo Calvino569faee2022-11-23 10:31:18 +0100337 "OSMMON_COLLECTOR_VM_INFRA_METRICS": self.config["vm-infra-metrics"],
beierlm8ea1f372022-06-30 09:02:30 -0400338 # Kafka configuration
339 "OSMMON_MESSAGE_DRIVER": "kafka",
340 "OSMMON_MESSAGE_HOST": self.kafka.host,
341 "OSMMON_MESSAGE_PORT": self.kafka.port,
342 # Database configuration
343 "OSMMON_DATABASE_DRIVER": "mongo",
Dario Faccin56db64f2023-02-16 20:50:38 +0100344 "OSMMON_DATABASE_URI": self._get_mongodb_uri(),
beierlm8ea1f372022-06-30 09:02:30 -0400345 "OSMMON_DATABASE_COMMONKEY": self.config["database-commonkey"],
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000346 # Prometheus configuration
347 "OSMMON_PROMETHEUS_URL": self.config.get("prometheus-url", ""),
348 "OSMMON_PROMETHEUS_USER": "",
349 "OSMMON_PROMETHEUS_PASSWORD": "",
350 # Grafana configuration
Patricia Reinoso87b620a2023-06-20 15:23:47 +0000351 "OSMMON_GRAFANA_URL": self.config.get("grafana-url", ""),
352 "OSMMON_GRAFANA_USER": self.config.get("grafana-user", ""),
353 "OSMMON_GRAFANA_PASSWORD": self.config.get("grafana-password", ""),
Mark Beierl2aa62e62022-10-13 12:55:26 -0400354 "OSMMON_KEYSTONE_ENABLED": self.config["keystone-enabled"],
355 "OSMMON_KEYSTONE_URL": self.keystone_client.host,
Patricia Reinosoc5b62062023-06-26 16:31:17 +0000356 # Keystone configuration
Mark Beierl2aa62e62022-10-13 12:55:26 -0400357 "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
358 "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
Guillermo Calvino569faee2022-11-23 10:31:18 +0100359 "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
Mark Beierl2aa62e62022-10-13 12:55:26 -0400360 "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
361 "OSMMON_KEYSTONE_SERVICE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
beierlm8ea1f372022-06-30 09:02:30 -0400362 }
Mark Beierl2aa62e62022-10-13 12:55:26 -0400363 logger.info(f"{environment}")
beierlm8ea1f372022-06-30 09:02:30 -0400364 if self.vca.data:
365 environment["OSMMON_VCA_HOST"] = self.vca.data.endpoints
Guillermo Calvino7da7a332022-08-19 09:47:32 +0200366 environment["OSMMON_VCA_SECRET"] = self.vca.data.secret
367 environment["OSMMON_VCA_USER"] = self.vca.data.user
368 environment["OSMMON_VCA_CACERT"] = self.vca.data.cacert
beierlm8ea1f372022-06-30 09:02:30 -0400369 return {
370 "summary": "mon layer",
371 "description": "pebble config layer for mon",
372 "services": {
373 self.service_name: {
374 "override": "replace",
375 "summary": "mon service",
376 "command": "/bin/bash scripts/start.sh",
377 "startup": "enabled",
378 "user": "appuser",
379 "group": "appuser",
380 "environment": environment,
381 }
382 },
383 }
384
Dario Faccin56db64f2023-02-16 20:50:38 +0100385 def _get_mongodb_uri(self):
386 return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
387
beierlm8ea1f372022-06-30 09:02:30 -0400388 def _patch_k8s_service(self) -> None:
389 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
390 self.service_patcher = KubernetesServicePatch(self, [port])
391
392
393if __name__ == "__main__": # pragma: no cover
394 main(OsmMonCharm)