LCM sidecar charm
[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 self._configure_service(self.container)
106
107 # Update charm status
108 self._on_update_status()
109 except CharmError as e:
110 logger.debug(e.message)
111 self.unit.status = e.status
112
113 def _on_update_status(self, _=None) -> None:
114 """Handler for the update-status event."""
115 try:
116 self._validate_config()
117 self._check_relations()
118 check_container_ready(self.container)
119 if self.debug_mode.started:
120 return
121 check_service_active(self.container, self.service_name)
122 self.unit.status = ActiveStatus()
123 except CharmError as e:
124 logger.debug(e.message)
125 self.unit.status = e.status
126
127 def _on_required_relation_broken(self, _) -> None:
128 """Handler for required relation-broken events."""
129 try:
130 check_container_ready(self.container)
131 check_service_active(self.container, self.service_name)
132 self.container.stop(self.container_name)
133 except CharmError:
134 pass
135 self._on_update_status()
136
137 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
138 """Handler for the get-debug-mode-information action event."""
139 if not self.debug_mode.started:
140 event.fail(
141 f"debug-mode has not started. Hint: juju config {self.app.name} debug-mode=true"
142 )
143 return
144
145 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
146 event.set_results(debug_info)
147
148 # ---------------------------------------------------------------------------
149 # Validation, configuration and more
150 # ---------------------------------------------------------------------------
151
152 def _validate_config(self) -> None:
153 """Validate charm configuration.
154
155 Raises:
156 CharmError: if charm configuration is invalid.
157 """
158 logger.debug("validating charm config")
159 if self.config["log-level"].upper() not in [
160 "TRACE",
161 "DEBUG",
162 "INFO",
163 "WARN",
164 "ERROR",
165 "FATAL",
166 ]:
167 raise CharmError("invalid value for log-level option")
168
169 def _observe_charm_events(self) -> None:
170 event_handler_mapping = {
171 # Core lifecycle events
172 self.on.lcm_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.kafka_available: self._on_config_changed,
177 self.on["kafka"].relation_broken: self._on_required_relation_broken,
178 self.on["mongodb"].relation_changed: self._on_config_changed,
179 self.on["mongodb"].relation_broken: self._on_required_relation_broken,
180 self.on["ro"].relation_changed: self._on_config_changed,
181 self.on["ro"].relation_broken: self._on_required_relation_broken,
182 self.on.vca_data_changed: self._on_config_changed,
183 self.on["vca"].relation_broken: self._on_config_changed,
184 # Action events
185 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
186 }
187 for event, handler in event_handler_mapping.items():
188 self.framework.observe(event, handler)
189
190 def _check_relations(self) -> None:
191 """Validate charm relations.
192
193 Raises:
194 CharmError: if charm configuration is invalid.
195 """
196 logger.debug("check for missing relations")
197 missing_relations = []
198
199 if not self.kafka.host or not self.kafka.port:
200 missing_relations.append("kafka")
201 if self.mongodb_client.is_missing_data_in_unit():
202 missing_relations.append("mongodb")
203 if not self.ro.host or not self.ro.port:
204 missing_relations.append("ro")
205
206 if missing_relations:
207 relations_str = ", ".join(missing_relations)
208 one_relation_missing = len(missing_relations) == 1
209 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
210 logger.warning(error_msg)
211 raise CharmError(error_msg)
212
213 def _configure_service(self, container: Container) -> None:
214 """Add Pebble layer with the lcm service."""
215 logger.debug(f"configuring {self.app.name} service")
216 container.add_layer("lcm", self._get_layer(), combine=True)
217 container.replan()
218
219 def _get_layer(self) -> Dict[str, Any]:
220 """Get layer for Pebble."""
221 environments = {
222 # General configuration
223 "OSMLCM_GLOBAL_LOGLEVEL": self.config["log-level"].upper(),
224 # Kafka configuration
225 "OSMLCM_MESSAGE_DRIVER": "kafka",
226 "OSMLCM_MESSAGE_HOST": self.kafka.host,
227 "OSMLCM_MESSAGE_PORT": self.kafka.port,
228 # RO configuration
229 "OSMLCM_RO_HOST": self.ro.host,
230 "OSMLCM_RO_PORT": self.ro.port,
231 "OSMLCM_RO_TENANT": "osm",
232 # Database configuration
233 "OSMLCM_DATABASE_DRIVER": "mongo",
234 "OSMLCM_DATABASE_URI": self.mongodb_client.connection_string,
235 "OSMLCM_DATABASE_COMMONKEY": self.config["database-commonkey"],
236 # Storage configuration
237 "OSMLCM_STORAGE_DRIVER": "mongo",
238 "OSMLCM_STORAGE_PATH": "/app/storage",
239 "OSMLCM_STORAGE_COLLECTION": "files",
240 "OSMLCM_STORAGE_URI": self.mongodb_client.connection_string,
241 "OSMLCM_VCA_HELM_CA_CERTS": self.config["helm-ca-certs"],
242 "OSMLCM_VCA_STABLEREPOURL": self.config["helm-stable-repo-url"],
243 }
244 # Vca configuration
245 if self.vca.data:
246 environments["OSMLCM_VCA_ENDPOINTS"] = self.vca.data.endpoints
247 environments["OSMLCM_VCA_USER"] = self.vca.data.user
248 environments["OSMLCM_VCA_PUBKEY"] = self.vca.data.public_key
249 environments["OSMLCM_VCA_SECRET"] = self.vca.data.secret
250 environments["OSMLCM_VCA_CACERT"] = self.vca.data.cacert
251 if self.vca.data.lxd_cloud:
252 environments["OSMLCM_VCA_CLOUD"] = self.vca.data.lxd_cloud
253
254 if self.vca.data.k8s_cloud:
255 environments["OSMLCM_VCA_K8S_CLOUD"] = self.vca.data.k8s_cloud
256 for key, value in self.vca.data.model_configs.items():
257 env_name = f'OSMLCM_VCA_MODEL_CONFIG_{key.upper().replace("-","_")}'
258 environments[env_name] = value
259
260 layer_config = {
261 "summary": "lcm layer",
262 "description": "pebble config layer for nbi",
263 "services": {
264 self.service_name: {
265 "override": "replace",
266 "summary": "lcm service",
267 "command": "python3 -m osm_lcm.lcm",
268 "startup": "enabled",
269 "user": "appuser",
270 "group": "appuser",
271 "environment": environments,
272 }
273 },
274 }
275 return layer_config
276
277
278 if __name__ == "__main__": # pragma: no cover
279 main(OsmLcmCharm)