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