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