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