Add POL side-car charm
[osm/devops.git] / installers / charm / osm-pol / 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 POL charm.
26
27 See more: https://charmhub.io/osm
28 """
29
30 import logging
31 from typing import Any, Dict
32
33 from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
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 ops.charm import ActionEvent, CharmBase
42 from ops.framework import StoredState
43 from ops.main import main
44 from ops.model import ActiveStatus, Container
45
46 from legacy_interfaces import MongoClient, MysqlClient
47
48 HOSTPATHS = [
49 HostPath(
50 config="pol-hostpath",
51 container_path="/usr/lib/python3/dist-packages/osm_policy_module",
52 ),
53 HostPath(
54 config="common-hostpath",
55 container_path="/usr/lib/python3/dist-packages/osm_common",
56 ),
57 ]
58
59 logger = logging.getLogger(__name__)
60
61
62 class OsmPolCharm(CharmBase):
63 """OSM POL Kubernetes sidecar charm."""
64
65 on = KafkaEvents()
66 _stored = StoredState()
67 container_name = "pol"
68 service_name = "pol"
69
70 def __init__(self, *args):
71 super().__init__(*args)
72
73 self.kafka = KafkaRequires(self)
74 self.mongodb_client = MongoClient(self, "mongodb")
75 self.mysql_client = MysqlClient(self, "mysql")
76 self._observe_charm_events()
77 self.container: Container = self.unit.get_container(self.container_name)
78 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
79
80 # ---------------------------------------------------------------------------
81 # Handlers for Charm Events
82 # ---------------------------------------------------------------------------
83
84 def _on_config_changed(self, _) -> None:
85 """Handler for the config-changed event."""
86 try:
87 self._validate_config()
88 self._check_relations()
89 # Check if the container is ready.
90 # Eventually it will become ready after the first pebble-ready event.
91 check_container_ready(self.container)
92
93 self._configure_service(self.container)
94 # Update charm status
95 self._on_update_status()
96 except CharmError as e:
97 logger.debug(e.message)
98 self.unit.status = e.status
99
100 def _on_update_status(self, _=None) -> None:
101 """Handler for the update-status event."""
102 try:
103 self._validate_config()
104 self._check_relations()
105 check_container_ready(self.container)
106 if self.debug_mode.started:
107 return
108 check_service_active(self.container, self.service_name)
109 self.unit.status = ActiveStatus()
110 except CharmError as e:
111 logger.debug(e.message)
112 self.unit.status = e.status
113
114 def _on_required_relation_broken(self, _) -> None:
115 """Handler for the kafka-broken event."""
116 # Check Pebble has started in the container
117 try:
118 check_container_ready(self.container)
119 check_service_active(self.container, self.service_name)
120 self.container.stop(self.container_name)
121 except CharmError:
122 pass
123 self._on_update_status()
124
125 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
126 """Handler for the get-debug-mode-information action event."""
127 if not self.debug_mode.started:
128 event.fail("debug-mode has not started. Hint: juju config pol debug-mode=true")
129 return
130
131 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
132 event.set_results(debug_info)
133
134 # ---------------------------------------------------------------------------
135 # Validation and configuration and more
136 # ---------------------------------------------------------------------------
137
138 def _observe_charm_events(self) -> None:
139 event_handler_mapping = {
140 # Core lifecycle events
141 self.on.pol_pebble_ready: self._on_config_changed,
142 self.on.config_changed: self._on_config_changed,
143 self.on.update_status: self._on_update_status,
144 # Relation events
145 self.on.kafka_available: self._on_config_changed,
146 self.on["kafka"].relation_broken: self._on_required_relation_broken,
147 # Action events
148 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
149 }
150 for relation in [self.on[rel_name] for rel_name in ["mongodb", "mysql"]]:
151 event_handler_mapping[relation.relation_changed] = self._on_config_changed
152 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
153
154 for event, handler in event_handler_mapping.items():
155 self.framework.observe(event, handler)
156
157 def _validate_config(self) -> None:
158 """Validate charm configuration.
159
160 Raises:
161 CharmError: if charm configuration is invalid.
162 """
163 logger.debug("validating charm config")
164
165 def _check_relations(self) -> None:
166 """Validate charm relations.
167
168 Raises:
169 CharmError: if charm configuration is invalid.
170 """
171 logger.debug("check for missing relations")
172 missing_relations = []
173
174 if not self.kafka.host or not self.kafka.port:
175 missing_relations.append("kafka")
176 if self.mongodb_client.is_missing_data_in_unit():
177 missing_relations.append("mongodb")
178 if not self.config.get("mysql-uri") and self.mysql_client.is_missing_data_in_unit():
179 missing_relations.append("mysql")
180
181 if missing_relations:
182 relations_str = ", ".join(missing_relations)
183 one_relation_missing = len(missing_relations) == 1
184 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
185 logger.warning(error_msg)
186 raise CharmError(error_msg)
187
188 def _configure_service(self, container: Container) -> None:
189 """Add Pebble layer with the pol service."""
190 logger.debug(f"configuring {self.app.name} service")
191 container.add_layer("pol", self._get_layer(), combine=True)
192 container.replan()
193
194 def _get_layer(self) -> Dict[str, Any]:
195 """Get layer for Pebble."""
196 return {
197 "summary": "pol layer",
198 "description": "pebble config layer for pol",
199 "services": {
200 self.service_name: {
201 "override": "replace",
202 "summary": "pol service",
203 "command": "/bin/bash scripts/start.sh",
204 "startup": "enabled",
205 "user": "appuser",
206 "group": "appuser",
207 "environment": {
208 # General configuration
209 "OSMPOL_GLOBAL_LOGLEVEL": self.config["log-level"],
210 # Kafka configuration
211 "OSMPOL_MESSAGE_HOST": self.kafka.host,
212 "OSMPOL_MESSAGE_PORT": self.kafka.port,
213 "OSMPOL_MESSAGE_DRIVER": "kafka",
214 # Database Mongodb configuration
215 "OSMPOL_DATABASE_DRIVER": "mongo",
216 "OSMPOL_DATABASE_URI": self.mongodb_client.connection_string,
217 # Database MySQL configuration
218 "OSMPOL_SQL_DATABASE_URI": self._get_mysql_uri(),
219 },
220 }
221 },
222 }
223
224 def _get_mysql_uri(self):
225 return self.config.get("mysql-uri") or self.mysql_client.get_root_uri("pol")
226
227
228 if __name__ == "__main__": # pragma: no cover
229 main(OsmPolCharm)