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