Revert "Revert "Feature 11010: Use of upstream helm charts for Prometheus and Grafana...
[osm/devops.git] / installers / charm / osm-lcm / 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 LCM 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.osm_libs.v0.utils import (
36 CharmError,
37 DebugMode,
38 HostPath,
39 check_container_ready,
40 check_service_active,
41 )
42 from charms.osm_ro.v0.ro import RoRequires
43 from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
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 HOSTPATHS = [
50 HostPath(
51 config="lcm-hostpath",
52 container_path="/usr/lib/python3/dist-packages/osm_lcm",
53 ),
54 HostPath(
55 config="common-hostpath",
56 container_path="/usr/lib/python3/dist-packages/osm_common",
57 ),
58 HostPath(
59 config="n2vc-hostpath",
60 container_path="/usr/lib/python3/dist-packages/n2vc",
61 ),
62 ]
63
64 logger = logging.getLogger(__name__)
65
66
67 class LcmEvents(CharmEvents):
68 """LCM events."""
69
70 vca_data_changed = EventSource(VcaDataChangedEvent)
71 kafka_available = EventSource(_KafkaAvailableEvent)
72
73
74 class OsmLcmCharm(CharmBase):
75 """OSM LCM Kubernetes sidecar charm."""
76
77 container_name = "lcm"
78 service_name = "lcm"
79 on = LcmEvents()
80 _stored = StoredState()
81
82 def __init__(self, *args):
83 super().__init__(*args)
84 self.vca = VcaRequires(self)
85 self.kafka = KafkaRequires(self)
86 self.mongodb_client = DatabaseRequires(
87 self, "mongodb", database_name="osm", extra_user_roles="admin"
88 )
89 self._observe_charm_events()
90 self.ro = RoRequires(self)
91 self.container: Container = self.unit.get_container(self.container_name)
92 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
93
94 # ---------------------------------------------------------------------------
95 # Handlers for Charm Events
96 # ---------------------------------------------------------------------------
97
98 def _on_config_changed(self, _) -> None:
99 """Handler for the config-changed event."""
100 try:
101 self._validate_config()
102 self._check_relations()
103 # Check if the container is ready.
104 # Eventually it will become ready after the first pebble-ready event.
105 check_container_ready(self.container)
106 if not self.debug_mode.started:
107 self._configure_service(self.container)
108
109 # Update charm status
110 self._on_update_status()
111 except CharmError as e:
112 logger.debug(e.message)
113 self.unit.status = e.status
114
115 def _on_update_status(self, _=None) -> None:
116 """Handler for the update-status event."""
117 try:
118 self._validate_config()
119 self._check_relations()
120 check_container_ready(self.container)
121 if self.debug_mode.started:
122 return
123 check_service_active(self.container, self.service_name)
124 self.unit.status = ActiveStatus()
125 except CharmError as e:
126 logger.debug(e.message)
127 self.unit.status = e.status
128
129 def _on_required_relation_broken(self, _) -> None:
130 """Handler for required relation-broken events."""
131 try:
132 check_container_ready(self.container)
133 check_service_active(self.container, self.service_name)
134 self.container.stop(self.container_name)
135 except CharmError:
136 pass
137 self._on_update_status()
138
139 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
140 """Handler for the get-debug-mode-information action event."""
141 if not self.debug_mode.started:
142 event.fail(
143 f"debug-mode has not started. Hint: juju config {self.app.name} debug-mode=true"
144 )
145 return
146
147 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
148 event.set_results(debug_info)
149
150 # ---------------------------------------------------------------------------
151 # Validation, configuration and more
152 # ---------------------------------------------------------------------------
153
154 def _validate_config(self) -> None:
155 """Validate charm configuration.
156
157 Raises:
158 CharmError: if charm configuration is invalid.
159 """
160 logger.debug("validating charm config")
161 if self.config["log-level"].upper() not in [
162 "TRACE",
163 "DEBUG",
164 "INFO",
165 "WARN",
166 "ERROR",
167 "FATAL",
168 ]:
169 raise CharmError("invalid value for log-level option")
170
171 def _observe_charm_events(self) -> None:
172 event_handler_mapping = {
173 # Core lifecycle events
174 self.on.lcm_pebble_ready: self._on_config_changed,
175 self.on.config_changed: self._on_config_changed,
176 self.on.update_status: self._on_update_status,
177 # Relation events
178 self.on.kafka_available: self._on_config_changed,
179 self.on["kafka"].relation_broken: self._on_required_relation_broken,
180 self.mongodb_client.on.database_created: self._on_config_changed,
181 self.on["mongodb"].relation_broken: self._on_required_relation_broken,
182 self.on["ro"].relation_changed: self._on_config_changed,
183 self.on["ro"].relation_broken: self._on_required_relation_broken,
184 self.on.vca_data_changed: self._on_config_changed,
185 self.on["vca"].relation_broken: self._on_config_changed,
186 # Action events
187 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
188 }
189 for event, handler in event_handler_mapping.items():
190 self.framework.observe(event, handler)
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 not self._is_database_available():
204 missing_relations.append("mongodb")
205 if not self.ro.host or not self.ro.port:
206 missing_relations.append("ro")
207
208 if missing_relations:
209 relations_str = ", ".join(missing_relations)
210 one_relation_missing = len(missing_relations) == 1
211 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
212 logger.warning(error_msg)
213 raise CharmError(error_msg)
214
215 def _is_database_available(self) -> bool:
216 try:
217 return self.mongodb_client.is_resource_created()
218 except KeyError:
219 return False
220
221 def _configure_service(self, container: Container) -> None:
222 """Add Pebble layer with the lcm service."""
223 logger.debug(f"configuring {self.app.name} service")
224 container.add_layer("lcm", self._get_layer(), combine=True)
225 container.replan()
226
227 def _get_layer(self) -> Dict[str, Any]:
228 """Get layer for Pebble."""
229 environments = {
230 # General configuration
231 "OSMLCM_GLOBAL_LOGLEVEL": self.config["log-level"].upper(),
232 # Kafka configuration
233 "OSMLCM_MESSAGE_DRIVER": "kafka",
234 "OSMLCM_MESSAGE_HOST": self.kafka.host,
235 "OSMLCM_MESSAGE_PORT": self.kafka.port,
236 # RO configuration
237 "OSMLCM_RO_HOST": self.ro.host,
238 "OSMLCM_RO_PORT": self.ro.port,
239 "OSMLCM_RO_TENANT": "osm",
240 # Database configuration
241 "OSMLCM_DATABASE_DRIVER": "mongo",
242 "OSMLCM_DATABASE_URI": self._get_mongodb_uri(),
243 "OSMLCM_DATABASE_COMMONKEY": self.config["database-commonkey"],
244 # Storage configuration
245 "OSMLCM_STORAGE_DRIVER": "mongo",
246 "OSMLCM_STORAGE_PATH": "/app/storage",
247 "OSMLCM_STORAGE_COLLECTION": "files",
248 "OSMLCM_STORAGE_URI": self._get_mongodb_uri(),
249 "OSMLCM_VCA_HELM_CA_CERTS": self.config["helm-ca-certs"],
250 "OSMLCM_VCA_STABLEREPOURL": self.config["helm-stable-repo-url"],
251 }
252 # Vca configuration
253 if self.vca.data:
254 environments["OSMLCM_VCA_ENDPOINTS"] = self.vca.data.endpoints
255 environments["OSMLCM_VCA_USER"] = self.vca.data.user
256 environments["OSMLCM_VCA_PUBKEY"] = self.vca.data.public_key
257 environments["OSMLCM_VCA_SECRET"] = self.vca.data.secret
258 environments["OSMLCM_VCA_CACERT"] = self.vca.data.cacert
259 if self.vca.data.lxd_cloud:
260 environments["OSMLCM_VCA_CLOUD"] = self.vca.data.lxd_cloud
261
262 if self.vca.data.k8s_cloud:
263 environments["OSMLCM_VCA_K8S_CLOUD"] = self.vca.data.k8s_cloud
264 for key, value in self.vca.data.model_configs.items():
265 env_name = f'OSMLCM_VCA_MODEL_CONFIG_{key.upper().replace("-","_")}'
266 environments[env_name] = value
267
268 layer_config = {
269 "summary": "lcm layer",
270 "description": "pebble config layer for nbi",
271 "services": {
272 self.service_name: {
273 "override": "replace",
274 "summary": "lcm service",
275 "command": "python3 -m osm_lcm.lcm",
276 "startup": "enabled",
277 "user": "appuser",
278 "group": "appuser",
279 "environment": environments,
280 }
281 },
282 }
283 return layer_config
284
285 def _get_mongodb_uri(self):
286 return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
287
288
289 if __name__ == "__main__": # pragma: no cover
290 main(OsmLcmCharm)