Fix bug 2142: Debug mode in Pebble Charms is not working
[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 if not self.debug_mode.started:
94 self._configure_service(self.container)
95 # Update charm status
96 self._on_update_status()
97 except CharmError as e:
98 logger.debug(e.message)
99 self.unit.status = e.status
100
101 def _on_update_status(self, _=None) -> None:
102 """Handler for the update-status event."""
103 try:
104 self._validate_config()
105 self._check_relations()
106 check_container_ready(self.container)
107 if self.debug_mode.started:
108 return
109 check_service_active(self.container, self.service_name)
110 self.unit.status = ActiveStatus()
111 except CharmError as e:
112 logger.debug(e.message)
113 self.unit.status = e.status
114
115 def _on_required_relation_broken(self, _) -> None:
116 """Handler for the kafka-broken event."""
117 # Check Pebble has started in the container
118 try:
119 check_container_ready(self.container)
120 check_service_active(self.container, self.service_name)
121 self.container.stop(self.container_name)
122 except CharmError:
123 pass
124 self._on_update_status()
125
126 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
127 """Handler for the get-debug-mode-information action event."""
128 if not self.debug_mode.started:
129 event.fail("debug-mode has not started. Hint: juju config pol debug-mode=true")
130 return
131
132 debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
133 event.set_results(debug_info)
134
135 # ---------------------------------------------------------------------------
136 # Validation and configuration and more
137 # ---------------------------------------------------------------------------
138
139 def _observe_charm_events(self) -> None:
140 event_handler_mapping = {
141 # Core lifecycle events
142 self.on.pol_pebble_ready: self._on_config_changed,
143 self.on.config_changed: self._on_config_changed,
144 self.on.update_status: self._on_update_status,
145 # Relation events
146 self.on.kafka_available: self._on_config_changed,
147 self.on["kafka"].relation_broken: self._on_required_relation_broken,
148 # Action events
149 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
150 }
151 for relation in [self.on[rel_name] for rel_name in ["mongodb", "mysql"]]:
152 event_handler_mapping[relation.relation_changed] = self._on_config_changed
153 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
154
155 for event, handler in event_handler_mapping.items():
156 self.framework.observe(event, handler)
157
158 def _validate_config(self) -> None:
159 """Validate charm configuration.
160
161 Raises:
162 CharmError: if charm configuration is invalid.
163 """
164 logger.debug("validating charm config")
165
166 def _check_relations(self) -> None:
167 """Validate charm relations.
168
169 Raises:
170 CharmError: if charm configuration is invalid.
171 """
172 logger.debug("check for missing relations")
173 missing_relations = []
174
175 if not self.kafka.host or not self.kafka.port:
176 missing_relations.append("kafka")
177 if self.mongodb_client.is_missing_data_in_unit():
178 missing_relations.append("mongodb")
179 if not self.config.get("mysql-uri") and self.mysql_client.is_missing_data_in_unit():
180 missing_relations.append("mysql")
181
182 if missing_relations:
183 relations_str = ", ".join(missing_relations)
184 one_relation_missing = len(missing_relations) == 1
185 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
186 logger.warning(error_msg)
187 raise CharmError(error_msg)
188
189 def _configure_service(self, container: Container) -> None:
190 """Add Pebble layer with the pol service."""
191 logger.debug(f"configuring {self.app.name} service")
192 container.add_layer("pol", self._get_layer(), combine=True)
193 container.replan()
194
195 def _get_layer(self) -> Dict[str, Any]:
196 """Get layer for Pebble."""
197 return {
198 "summary": "pol layer",
199 "description": "pebble config layer for pol",
200 "services": {
201 self.service_name: {
202 "override": "replace",
203 "summary": "pol service",
204 "command": "/bin/bash scripts/start.sh",
205 "startup": "enabled",
206 "user": "appuser",
207 "group": "appuser",
208 "environment": {
209 # General configuration
210 "OSMPOL_GLOBAL_LOGLEVEL": self.config["log-level"],
211 # Kafka configuration
212 "OSMPOL_MESSAGE_HOST": self.kafka.host,
213 "OSMPOL_MESSAGE_PORT": self.kafka.port,
214 "OSMPOL_MESSAGE_DRIVER": "kafka",
215 # Database Mongodb configuration
216 "OSMPOL_DATABASE_DRIVER": "mongo",
217 "OSMPOL_DATABASE_URI": self.mongodb_client.connection_string,
218 # Database MySQL configuration
219 "OSMPOL_SQL_DATABASE_URI": self._get_mysql_uri(),
220 },
221 }
222 },
223 }
224
225 def _get_mysql_uri(self):
226 return self.config.get("mysql-uri") or self.mysql_client.get_root_uri("pol")
227
228
229 if __name__ == "__main__": # pragma: no cover
230 main(OsmPolCharm)