Feature 11037 Installation of ingress controller in OSM community installer
[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.data_platform_libs.v0.data_interfaces import DatabaseRequires
35 from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
36 from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
37 from charms.osm_libs.v0.utils import (
38 CharmError,
39 DebugMode,
40 HostPath,
41 check_container_ready,
42 check_service_active,
43 )
44 from charms.osm_ro.v0.ro import RoProvides
45 from lightkube.models.core_v1 import ServicePort
46 from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent
47 from ops.framework import StoredState
48 from ops.main import main
49 from ops.model import ActiveStatus, Container
50
51 ro_host_paths = {
52 "NG-RO": "/usr/lib/python3/dist-packages/osm_ng_ro",
53 "RO-plugin": "/usr/lib/python3/dist-packages/osm_ro_plugin",
54 "RO-SDN-arista_cloudvision": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision",
55 "RO-SDN-dpb": "/usr/lib/python3/dist-packages/osm_rosdn_dpb",
56 "RO-SDN-dynpac": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac",
57 "RO-SDN-floodlight_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof",
58 "RO-SDN-ietfl2vpn": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn",
59 "RO-SDN-juniper_contrail": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail",
60 "RO-SDN-odl_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_odlof",
61 "RO-SDN-onos_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_onosof",
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 = DatabaseRequires(self, "mongodb", database_name="osm")
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.mongodb_client.on.database_created: 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 _is_database_available(self) -> bool:
210 try:
211 return self.mongodb_client.is_resource_created()
212 except KeyError:
213 return False
214
215 def _validate_config(self) -> None:
216 """Validate charm configuration.
217
218 Raises:
219 CharmError: if charm configuration is invalid.
220 """
221 logger.debug("validating charm config")
222 if self.config["log-level"].upper() not in [
223 "TRACE",
224 "DEBUG",
225 "INFO",
226 "WARN",
227 "ERROR",
228 "FATAL",
229 ]:
230 raise CharmError("invalid value for log-level option")
231
232 refresh_period = self.config.get("period_refresh_active")
233 if refresh_period and refresh_period < 60 and refresh_period != -1:
234 raise ValueError(
235 "Refresh Period is too tight, insert >= 60 seconds or disable using -1"
236 )
237
238 def _check_relations(self) -> None:
239 """Validate charm relations.
240
241 Raises:
242 CharmError: if charm configuration is invalid.
243 """
244 logger.debug("check for missing relations")
245 missing_relations = []
246
247 if not self.kafka.host or not self.kafka.port:
248 missing_relations.append("kafka")
249 if not self._is_database_available():
250 missing_relations.append("mongodb")
251
252 if missing_relations:
253 relations_str = ", ".join(missing_relations)
254 one_relation_missing = len(missing_relations) == 1
255 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
256 logger.warning(error_msg)
257 raise CharmError(error_msg)
258
259 def _configure_certificates(self) -> None:
260 """Push certificates to the RO container."""
261 if not (certificate_config := self.config.get("certificates")):
262 return
263
264 certificates_list = certificate_config.split(",")
265 updated_certificates = set()
266
267 for certificate in certificates_list:
268 if ":" not in certificate:
269 continue
270 name, content = certificate.split(":")
271 content = decode(content)
272 self.container.push(
273 f"/certs/{name}",
274 content,
275 permissions=0o400,
276 make_dirs=True,
277 user=USER,
278 group=GROUP,
279 )
280 updated_certificates.add(name)
281 self._stored.certificates.add(name)
282 logger.info(f"certificate {name} pushed successfully")
283
284 stored_certificates = {c for c in self._stored.certificates}
285 for certificate_to_remove in stored_certificates.difference(updated_certificates):
286 self.container.remove_path(f"/certs/{certificate_to_remove}")
287 self._stored.certificates.remove(certificate_to_remove)
288 logger.info(f"certificate {certificate_to_remove} removed successfully")
289
290 def _configure_service(self) -> None:
291 """Add Pebble layer with the ro service."""
292 logger.debug(f"configuring {self.app.name} service")
293 self.container.add_layer("ro", self._get_layer(), combine=True)
294 self.container.replan()
295
296 def _get_layer(self) -> Dict[str, Any]:
297 """Get layer for Pebble."""
298 return {
299 "summary": "ro layer",
300 "description": "pebble config layer for ro",
301 "services": {
302 "ro": {
303 "override": "replace",
304 "summary": "ro service",
305 "command": "/bin/sh -c 'cd /app/osm_ro && python3 -u -m osm_ng_ro.ro_main'", # cd /app/osm_nbi is needed until we upgrade Juju to 3.x.
306 "startup": "enabled",
307 "user": USER,
308 "group": GROUP,
309 "working-dir": "/app/osm_ro", # This parameter has no effect in Juju 2.9.x.
310 "environment": {
311 # General configuration
312 "OSMRO_LOG_LEVEL": self.config["log-level"].upper(),
313 # Kafka configuration
314 "OSMRO_MESSAGE_HOST": self.kafka.host,
315 "OSMRO_MESSAGE_PORT": self.kafka.port,
316 "OSMRO_MESSAGE_DRIVER": "kafka",
317 # Database configuration
318 "OSMRO_DATABASE_DRIVER": "mongo",
319 "OSMRO_DATABASE_URI": self._get_mongodb_uri(),
320 "OSMRO_DATABASE_COMMONKEY": self.config["database-commonkey"],
321 # Storage configuration
322 "OSMRO_STORAGE_DRIVER": "mongo",
323 "OSMRO_STORAGE_PATH": "/app/storage",
324 "OSMRO_STORAGE_COLLECTION": "files",
325 "OSMRO_STORAGE_URI": self._get_mongodb_uri(),
326 "OSMRO_PERIOD_REFRESH_ACTIVE": self.config.get("period_refresh_active")
327 or 60,
328 },
329 }
330 },
331 }
332
333 def _get_mongodb_uri(self):
334 return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
335
336
337 if __name__ == "__main__": # pragma: no cover
338 main(OsmRoCharm)