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