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