Fix bug 2142: Debug mode in Pebble Charms is not working
[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("debug-mode has not started. Hint: juju config mon debug-mode=true")
155 return
156
157 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
158 event.set_results(debug_info)
159
160 # ---------------------------------------------------------------------------
161 # Validation and configuration and more
162 # ---------------------------------------------------------------------------
163
164 def _observe_charm_events(self) -> None:
165 event_handler_mapping = {
166 # Core lifecycle events
167 self.on.mon_pebble_ready: self._on_config_changed,
168 self.on.config_changed: self._on_config_changed,
169 self.on.update_status: self._on_update_status,
170 # Relation events
171 self.on.vca_data_changed: self._on_config_changed,
172 self.on.kafka_available: self._on_config_changed,
173 self.on["kafka"].relation_broken: self._on_required_relation_broken,
174 # Action events
175 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
176 }
177 for relation in [self.on[rel_name] for rel_name in ["mongodb", "prometheus", "keystone"]]:
178 event_handler_mapping[relation.relation_changed] = self._on_config_changed
179 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
180
181 for event, handler in event_handler_mapping.items():
182 self.framework.observe(event, handler)
183
184 def _validate_config(self) -> None:
185 """Validate charm configuration.
186
187 Raises:
188 CharmError: if charm configuration is invalid.
189 """
190 logger.debug("validating charm config")
191
192 def _check_relations(self) -> None:
193 """Validate charm relations.
194
195 Raises:
196 CharmError: if charm configuration is invalid.
197 """
198 logger.debug("check for missing relations")
199 missing_relations = []
200
201 if not self.kafka.host or not self.kafka.port:
202 missing_relations.append("kafka")
203 if self.mongodb_client.is_missing_data_in_unit():
204 missing_relations.append("mongodb")
205 if self.prometheus_client.is_missing_data_in_app():
206 missing_relations.append("prometheus")
207 if self.keystone_client.is_missing_data_in_app():
208 missing_relations.append("keystone")
209
210 if missing_relations:
211 relations_str = ", ".join(missing_relations)
212 one_relation_missing = len(missing_relations) == 1
213 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
214 logger.warning(error_msg)
215 raise CharmError(error_msg)
216
217 def _configure_service(self, container: Container) -> None:
218 """Add Pebble layer with the mon service."""
219 logger.debug(f"configuring {self.app.name} service")
220 container.add_layer("mon", self._get_layer(), combine=True)
221 container.replan()
222
223 def _get_layer(self) -> Dict[str, Any]:
224 """Get layer for Pebble."""
225 environment = {
226 # General configuration
227 "OSMMON_GLOBAL_LOGLEVEL": self.config["log-level"],
228 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": self.config["openstack-default-granularity"],
229 "OSMMON_GLOBAL_REQUEST_TIMEOUT": self.config["global-request-timeout"],
230 "OSMMON_COLLECTOR_INTERVAL": self.config["collector-interval"],
231 "OSMMON_EVALUATOR_INTERVAL": self.config["evaluator-interval"],
232 # Kafka configuration
233 "OSMMON_MESSAGE_DRIVER": "kafka",
234 "OSMMON_MESSAGE_HOST": self.kafka.host,
235 "OSMMON_MESSAGE_PORT": self.kafka.port,
236 # Database configuration
237 "OSMMON_DATABASE_DRIVER": "mongo",
238 "OSMMON_DATABASE_URI": self.mongodb_client.connection_string,
239 "OSMMON_DATABASE_COMMONKEY": self.config["database-commonkey"],
240 # Prometheus/grafana configuration
241 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
242 "OSMMON_GRAFANA_URL": self.config["grafana-url"],
243 "OSMMON_GRAFANA_USER": self.config["grafana-user"],
244 "OSMMON_GRAFANA_PASSWORD": self.config["grafana-password"],
245 }
246 if self.vca.data:
247 environment["OSMMON_VCA_HOST"] = self.vca.data.endpoints
248 environment["OSMMON_VCA_SECRET"] = self.vca.secret
249 environment["OSMMON_VCA_USER"] = self.vca.user
250 environment["OSMMON_VCA_CACERT"] = self.vca.cacert
251 return {
252 "summary": "mon layer",
253 "description": "pebble config layer for mon",
254 "services": {
255 self.service_name: {
256 "override": "replace",
257 "summary": "mon service",
258 "command": "/bin/bash scripts/start.sh",
259 "startup": "enabled",
260 "user": "appuser",
261 "group": "appuser",
262 "environment": environment,
263 }
264 },
265 }
266
267 def _patch_k8s_service(self) -> None:
268 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
269 self.service_patcher = KubernetesServicePatch(self, [port])
270
271
272 if __name__ == "__main__": # pragma: no cover
273 main(OsmMonCharm)