Update Dockerfile and stage-test script to run tests for charms
[osm/devops.git] / installers / charm / osm-mon / 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 NBI 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 KafkaRequires, _KafkaAvailableEvent
34 from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
35 from charms.osm_libs.v0.utils import (
36 CharmError,
37 DebugMode,
38 HostPath,
39 check_container_ready,
40 check_service_active,
41 )
42 from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
43 from lightkube.models.core_v1 import ServicePort
44 from ops.charm import ActionEvent, CharmBase, CharmEvents
45 from ops.framework import EventSource, StoredState
46 from ops.main import main
47 from ops.model import ActiveStatus, Container
48
49 from legacy_interfaces import KeystoneClient, MongoClient, PrometheusClient
50
51 HOSTPATHS = [
52 HostPath(
53 config="mon-hostpath",
54 container_path="/usr/lib/python3/dist-packages/osm_mon",
55 ),
56 HostPath(
57 config="common-hostpath",
58 container_path="/usr/lib/python3/dist-packages/osm_common",
59 ),
60 HostPath(
61 config="n2vc-hostpath",
62 container_path="/usr/lib/python3/dist-packages/n2vc",
63 ),
64 ]
65 SERVICE_PORT = 8000
66
67 logger = logging.getLogger(__name__)
68
69
70 class MonEvents(CharmEvents):
71 """MON events."""
72
73 vca_data_changed = EventSource(VcaDataChangedEvent)
74 kafka_available = EventSource(_KafkaAvailableEvent)
75
76
77 class OsmMonCharm(CharmBase):
78 """OSM MON Kubernetes sidecar charm."""
79
80 on = MonEvents()
81 _stored = StoredState()
82 container_name = "mon"
83 service_name = "mon"
84
85 def __init__(self, *args):
86 super().__init__(*args)
87 self.kafka = KafkaRequires(self)
88 self.mongodb_client = MongoClient(self, "mongodb")
89 self.prometheus_client = PrometheusClient(self, "prometheus")
90 self.keystone_client = KeystoneClient(self, "keystone")
91 self.vca = VcaRequires(self)
92 self._observe_charm_events()
93 self.container: Container = self.unit.get_container(self.container_name)
94 self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
95 self._patch_k8s_service()
96
97 @property
98 def external_hostname(self) -> str:
99 """External hostname property.
100
101 Returns:
102 str: the external hostname from config.
103 If not set, return the ClusterIP service name.
104 """
105 return self.config.get("external-hostname") or self.app.name
106
107 # ---------------------------------------------------------------------------
108 # Handlers for Charm Events
109 # ---------------------------------------------------------------------------
110
111 def _on_config_changed(self, _) -> None:
112 """Handler for the config-changed event."""
113 try:
114 self._validate_config()
115 self._check_relations()
116 # Check if the container is ready.
117 # Eventually it will become ready after the first pebble-ready event.
118 check_container_ready(self.container)
119 if not self.debug_mode.started:
120 self._configure_service(self.container)
121 # Update charm status
122 self._on_update_status()
123 except CharmError as e:
124 logger.debug(e.message)
125 self.unit.status = e.status
126
127 def _on_update_status(self, _=None) -> None:
128 """Handler for the update-status event."""
129 try:
130 self._validate_config()
131 self._check_relations()
132 check_container_ready(self.container)
133 if self.debug_mode.started:
134 return
135 check_service_active(self.container, self.service_name)
136 self.unit.status = ActiveStatus()
137 except CharmError as e:
138 logger.debug(e.message)
139 self.unit.status = e.status
140
141 def _on_required_relation_broken(self, _) -> None:
142 """Handler for the kafka-broken event."""
143 try:
144 check_container_ready(self.container)
145 check_service_active(self.container, self.service_name)
146 self.container.stop(self.container_name)
147 except CharmError:
148 pass
149 self._on_update_status()
150
151 def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
152 """Handler for the get-debug-mode-information action event."""
153 if not self.debug_mode.started:
154 event.fail("debug-mode has not started. Hint: juju config mon debug-mode=true")
155 return
156
157 debug_info = {
158 "command": self.debug_mode.command,
159 "password": self.debug_mode.password,
160 }
161 event.set_results(debug_info)
162
163 # ---------------------------------------------------------------------------
164 # Validation and configuration and more
165 # ---------------------------------------------------------------------------
166
167 def _observe_charm_events(self) -> None:
168 event_handler_mapping = {
169 # Core lifecycle events
170 self.on.mon_pebble_ready: self._on_config_changed,
171 self.on.config_changed: self._on_config_changed,
172 self.on.update_status: self._on_update_status,
173 # Relation events
174 self.on.vca_data_changed: self._on_config_changed,
175 self.on.kafka_available: self._on_config_changed,
176 self.on["kafka"].relation_broken: self._on_required_relation_broken,
177 # Action events
178 self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
179 }
180 for relation in [self.on[rel_name] for rel_name in ["mongodb", "prometheus", "keystone"]]:
181 event_handler_mapping[relation.relation_changed] = self._on_config_changed
182 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
183
184 for event, handler in event_handler_mapping.items():
185 self.framework.observe(event, handler)
186
187 def _validate_config(self) -> None:
188 """Validate charm configuration.
189
190 Raises:
191 CharmError: if charm configuration is invalid.
192 """
193 logger.debug("validating charm config")
194
195 def _check_relations(self) -> None:
196 """Validate charm relations.
197
198 Raises:
199 CharmError: if charm configuration is invalid.
200 """
201 logger.debug("check for missing relations")
202 missing_relations = []
203
204 if not self.kafka.host or not self.kafka.port:
205 missing_relations.append("kafka")
206 if self.mongodb_client.is_missing_data_in_unit():
207 missing_relations.append("mongodb")
208 if self.prometheus_client.is_missing_data_in_app():
209 missing_relations.append("prometheus")
210 if self.keystone_client.is_missing_data_in_app():
211 missing_relations.append("keystone")
212
213 if missing_relations:
214 relations_str = ", ".join(missing_relations)
215 one_relation_missing = len(missing_relations) == 1
216 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
217 logger.warning(error_msg)
218 raise CharmError(error_msg)
219
220 def _configure_service(self, container: Container) -> None:
221 """Add Pebble layer with the mon service."""
222 logger.debug(f"configuring {self.app.name} service")
223 container.add_layer("mon", self._get_layer(), combine=True)
224 container.replan()
225
226 def _get_layer(self) -> Dict[str, Any]:
227 """Get layer for Pebble."""
228 environment = {
229 # General configuration
230 "OSMMON_GLOBAL_LOGLEVEL": self.config["log-level"],
231 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": self.config["openstack-default-granularity"],
232 "OSMMON_GLOBAL_REQUEST_TIMEOUT": self.config["global-request-timeout"],
233 "OSMMON_COLLECTOR_INTERVAL": self.config["collector-interval"],
234 "OSMMON_EVALUATOR_INTERVAL": self.config["evaluator-interval"],
235 "OSMMON_COLLECTOR_VM_INFRA_METRICS": self.config["vm-infra-metrics"],
236 # Kafka configuration
237 "OSMMON_MESSAGE_DRIVER": "kafka",
238 "OSMMON_MESSAGE_HOST": self.kafka.host,
239 "OSMMON_MESSAGE_PORT": self.kafka.port,
240 # Database configuration
241 "OSMMON_DATABASE_DRIVER": "mongo",
242 "OSMMON_DATABASE_URI": self.mongodb_client.connection_string,
243 "OSMMON_DATABASE_COMMONKEY": self.config["database-commonkey"],
244 # Prometheus/grafana configuration
245 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
246 "OSMMON_PROMETHEUS_USER": self.prometheus_client.user,
247 "OSMMON_PROMETHEUS_PASSWORD": self.prometheus_client.password,
248 "OSMMON_GRAFANA_URL": self.config["grafana-url"],
249 "OSMMON_GRAFANA_USER": self.config["grafana-user"],
250 "OSMMON_GRAFANA_PASSWORD": self.config["grafana-password"],
251 "OSMMON_KEYSTONE_ENABLED": self.config["keystone-enabled"],
252 "OSMMON_KEYSTONE_URL": self.keystone_client.host,
253 "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
254 "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
255 "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
256 "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
257 "OSMMON_KEYSTONE_SERVICE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
258 }
259 logger.info(f"{environment}")
260 if self.vca.data:
261 environment["OSMMON_VCA_HOST"] = self.vca.data.endpoints
262 environment["OSMMON_VCA_SECRET"] = self.vca.data.secret
263 environment["OSMMON_VCA_USER"] = self.vca.data.user
264 environment["OSMMON_VCA_CACERT"] = self.vca.data.cacert
265 return {
266 "summary": "mon layer",
267 "description": "pebble config layer for mon",
268 "services": {
269 self.service_name: {
270 "override": "replace",
271 "summary": "mon service",
272 "command": "/bin/bash scripts/start.sh",
273 "startup": "enabled",
274 "user": "appuser",
275 "group": "appuser",
276 "environment": environment,
277 }
278 },
279 }
280
281 def _patch_k8s_service(self) -> None:
282 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
283 self.service_patcher = KubernetesServicePatch(self, [port])
284
285
286 if __name__ == "__main__": # pragma: no cover
287 main(OsmMonCharm)