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