Fixing hostpaths of RO side car charm, updating osm_lib library.
[osm/devops.git] / installers / charm / osm-ro / 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 RO charm.
26
27 See more: https://charmhub.io/osm
28 """
29
30 import base64
31 import logging
32 from typing import Any, Dict
33
34 from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
35 from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
36 from charms.osm_libs.v0.utils import (
37 CharmError,
38 DebugMode,
39 HostPath,
40 check_container_ready,
41 check_service_active,
42 )
43 from charms.osm_ro.v0.ro import RoProvides
44 from lightkube.models.core_v1 import ServicePort
45 from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent
46 from ops.framework import StoredState
47 from ops.main import main
48 from ops.model import ActiveStatus, Container
49
50 from legacy_interfaces import MongoClient
51
52 ro_host_paths = {
53 "NG-RO": "/usr/lib/python3/dist-packages/osm_ng_ro",
54 "RO-plugin": "/usr/lib/python3/dist-packages/osm_ro_plugin",
55 "RO-SDN-arista_cloudvision": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision",
56 "RO-SDN-dpb": "/usr/lib/python3/dist-packages/osm_rosdn_dpb",
57 "RO-SDN-dynpac": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac",
58 "RO-SDN-floodlight_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof",
59 "RO-SDN-ietfl2vpn": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn",
60 "RO-SDN-juniper_contrail": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail",
61 "RO-SDN-odl_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_odlof",
62 "RO-SDN-onos_vpls": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls",
63 "RO-VIM-aws": "/usr/lib/python3/dist-packages/osm_rovim_aws",
64 "RO-VIM-azure": "/usr/lib/python3/dist-packages/osm_rovim_azure",
65 "RO-VIM-gcp": "/usr/lib/python3/dist-packages/osm_rovim_gcp",
66 "RO-VIM-openstack": "/usr/lib/python3/dist-packages/osm_rovim_openstack",
67 "RO-VIM-openvim": "/usr/lib/python3/dist-packages/osm_rovim_openvim",
68 "RO-VIM-vmware": "/usr/lib/python3/dist-packages/osm_rovim_vmware",
69 }
70 HOSTPATHS = [
71 HostPath(
72 config="ro-hostpath",
73 container_path="/usr/lib/python3/dist-packages/",
74 submodules=ro_host_paths,
75 ),
76 HostPath(
77 config="common-hostpath",
78 container_path="/usr/lib/python3/dist-packages/osm_common",
79 ),
80 ]
81 SERVICE_PORT = 9090
82 USER = GROUP = "appuser"
83
84 logger = logging.getLogger(__name__)
85
86
87 def decode(content: str):
88 """Base64 decoding of a string."""
89 return base64.b64decode(content.encode("utf-8")).decode("utf-8")
90
91
92 class OsmRoCharm(CharmBase):
93 """OSM RO Kubernetes sidecar charm."""
94
95 on = KafkaEvents()
96 service_name = "ro"
97 _stored = StoredState()
98
99 def __init__(self, *args):
100 super().__init__(*args)
101 self._stored.set_default(certificates=set())
102 self.kafka = KafkaRequires(self)
103 self.mongodb_client = MongoClient(self, "mongodb")
104 self._observe_charm_events()
105 self._patch_k8s_service()
106 self.ro = RoProvides(self)
107 self.container: Container = self.unit.get_container("ro")
108 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
109
110 # ---------------------------------------------------------------------------
111 # Handlers for Charm Events
112 # ---------------------------------------------------------------------------
113
114 def _on_config_changed(self, _) -> None:
115 """Handler for the config-changed event."""
116 try:
117 self._validate_config()
118 self._check_relations()
119 # Check if the container is ready.
120 # Eventually it will become ready after the first pebble-ready event.
121 check_container_ready(self.container)
122
123 self._configure_certificates()
124 if not self.debug_mode.started:
125 self._configure_service()
126 self._update_ro_relation()
127
128 # Update charm status
129 self._on_update_status()
130 except CharmError as e:
131 logger.debug(e.message)
132 self.unit.status = e.status
133
134 def _on_update_status(self, _=None) -> None:
135 """Handler for the update-status event."""
136 try:
137 self._validate_config()
138 self._check_relations()
139 check_container_ready(self.container)
140 if self.debug_mode.started:
141 return
142 check_service_active(self.container, self.service_name)
143 self.unit.status = ActiveStatus()
144 except CharmError as e:
145 logger.debug(e.message)
146 self.unit.status = e.status
147
148 def _on_required_relation_broken(self, _) -> None:
149 """Handler for the kafka-broken event."""
150 try:
151 check_container_ready(self.container)
152 check_service_active(self.container, "ro")
153 self.container.stop("ro")
154 except CharmError:
155 pass
156
157 self._on_update_status()
158
159 def _update_ro_relation(self, event: RelationJoinedEvent = None) -> None:
160 """Handler for the ro-relation-joined event."""
161 try:
162 if self.unit.is_leader():
163 check_container_ready(self.container)
164 check_service_active(self.container, "ro")
165 self.ro.set_host_info(
166 self.app.name, SERVICE_PORT, event.relation if event else None
167 )
168 except CharmError as e:
169 self.unit.status = e.status
170
171 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
172 """Handler for the get-debug-mode-information action event."""
173 if not self.debug_mode.started:
174 event.fail(
175 f"debug-mode has not started. Hint: juju config {self.app.name} debug-mode=true"
176 )
177 return
178
179 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
180 event.set_results(debug_info)
181
182 # ---------------------------------------------------------------------------
183 # Validation and configuration and more
184 # ---------------------------------------------------------------------------
185
186 def _patch_k8s_service(self) -> None:
187 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
188 self.service_patcher = KubernetesServicePatch(self, [port])
189
190 def _observe_charm_events(self) -> None:
191 event_handler_mapping = {
192 # Core lifecycle events
193 self.on.ro_pebble_ready: self._on_config_changed,
194 self.on.config_changed: self._on_config_changed,
195 self.on.update_status: self._on_update_status,
196 # Relation events
197 self.on.kafka_available: self._on_config_changed,
198 self.on["kafka"].relation_broken: self._on_required_relation_broken,
199 self.on["mongodb"].relation_changed: self._on_config_changed,
200 self.on["mongodb"].relation_broken: self._on_required_relation_broken,
201 self.on.ro_relation_joined: self._update_ro_relation,
202 # Action events
203 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
204 }
205
206 for event, handler in event_handler_mapping.items():
207 self.framework.observe(event, handler)
208
209 def _validate_config(self) -> None:
210 """Validate charm configuration.
211
212 Raises:
213 CharmError: if charm configuration is invalid.
214 """
215 logger.debug("validating charm config")
216 if self.config["log-level"].upper() not in [
217 "TRACE",
218 "DEBUG",
219 "INFO",
220 "WARN",
221 "ERROR",
222 "FATAL",
223 ]:
224 raise CharmError("invalid value for log-level option")
225
226 refresh_period = self.config.get("period_refresh_active")
227 if refresh_period and refresh_period < 60 and refresh_period != -1:
228 raise ValueError(
229 "Refresh Period is too tight, insert >= 60 seconds or disable using -1"
230 )
231
232 def _check_relations(self) -> None:
233 """Validate charm relations.
234
235 Raises:
236 CharmError: if charm configuration is invalid.
237 """
238 logger.debug("check for missing relations")
239 missing_relations = []
240
241 if not self.kafka.host or not self.kafka.port:
242 missing_relations.append("kafka")
243 if self.mongodb_client.is_missing_data_in_unit():
244 missing_relations.append("mongodb")
245
246 if missing_relations:
247 relations_str = ", ".join(missing_relations)
248 one_relation_missing = len(missing_relations) == 1
249 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
250 logger.warning(error_msg)
251 raise CharmError(error_msg)
252
253 def _configure_certificates(self) -> None:
254 """Push certificates to the RO container."""
255 if not (certificate_config := self.config.get("certificates")):
256 return
257
258 certificates_list = certificate_config.split(",")
259 updated_certificates = set()
260
261 for certificate in certificates_list:
262 if ":" not in certificate:
263 continue
264 name, content = certificate.split(":")
265 content = decode(content)
266 self.container.push(
267 f"/certs/{name}",
268 content,
269 permissions=0o400,
270 make_dirs=True,
271 user=USER,
272 group=GROUP,
273 )
274 updated_certificates.add(name)
275 self._stored.certificates.add(name)
276 logger.info(f"certificate {name} pushed successfully")
277
278 stored_certificates = {c for c in self._stored.certificates}
279 for certificate_to_remove in stored_certificates.difference(updated_certificates):
280 self.container.remove_path(f"/certs/{certificate_to_remove}")
281 self._stored.certificates.remove(certificate_to_remove)
282 logger.info(f"certificate {certificate_to_remove} removed successfully")
283
284 def _configure_service(self) -> None:
285 """Add Pebble layer with the ro service."""
286 logger.debug(f"configuring {self.app.name} service")
287 self.container.add_layer("ro", self._get_layer(), combine=True)
288 self.container.replan()
289
290 def _get_layer(self) -> Dict[str, Any]:
291 """Get layer for Pebble."""
292 return {
293 "summary": "ro layer",
294 "description": "pebble config layer for ro",
295 "services": {
296 "ro": {
297 "override": "replace",
298 "summary": "ro service",
299 "command": "python3 -u -m osm_ng_ro.ro_main",
300 "startup": "enabled",
301 "user": USER,
302 "group": GROUP,
303 "environment": {
304 # General configuration
305 "OSMRO_LOG_LEVEL": self.config["log-level"].upper(),
306 # Kafka configuration
307 "OSMRO_MESSAGE_HOST": self.kafka.host,
308 "OSMRO_MESSAGE_PORT": self.kafka.port,
309 "OSMRO_MESSAGE_DRIVER": "kafka",
310 # Database configuration
311 "OSMRO_DATABASE_DRIVER": "mongo",
312 "OSMRO_DATABASE_URI": self.mongodb_client.connection_string,
313 "OSMRO_DATABASE_COMMONKEY": self.config["database-commonkey"],
314 # Storage configuration
315 "OSMRO_STORAGE_DRIVER": "mongo",
316 "OSMRO_STORAGE_PATH": "/app/storage",
317 "OSMRO_STORAGE_COLLECTION": "files",
318 "OSMRO_STORAGE_URI": self.mongodb_client.connection_string,
319 "OSMRO_PERIOD_REFRESH_ACTIVE": self.config.get("period_refresh_active")
320 or 60,
321 },
322 }
323 },
324 }
325
326
327 if __name__ == "__main__": # pragma: no cover
328 main(OsmRoCharm)