Fix 1539: Helm v2.17.0 and set default value for stablerepourl
[osm/devops.git] / installers / charm / lcm / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2021 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 # pylint: disable=E0213
24
25
26 import logging
27 from typing import NoReturn, Optional
28
29
30 from ops.main import main
31 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
32 from opslib.osm.interfaces.http import HttpClient
33 from opslib.osm.interfaces.kafka import KafkaClient
34 from opslib.osm.interfaces.mongo import MongoClient
35 from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder
36 from opslib.osm.validator import ModelValidator, validator
37
38
39 logger = logging.getLogger(__name__)
40
41 PORT = 9999
42
43
44 class ConfigModel(ModelValidator):
45 vca_host: Optional[str]
46 vca_port: Optional[int]
47 vca_user: Optional[str]
48 vca_secret: Optional[str]
49 vca_pubkey: Optional[str]
50 vca_cacert: Optional[str]
51 vca_cloud: Optional[str]
52 vca_k8s_cloud: Optional[str]
53 database_commonkey: str
54 mongodb_uri: Optional[str]
55 log_level: str
56 vca_apiproxy: Optional[str]
57 # Model-config options
58 vca_model_config_agent_metadata_url: Optional[str]
59 vca_model_config_agent_stream: Optional[str]
60 vca_model_config_apt_ftp_proxy: Optional[str]
61 vca_model_config_apt_http_proxy: Optional[str]
62 vca_model_config_apt_https_proxy: Optional[str]
63 vca_model_config_apt_mirror: Optional[str]
64 vca_model_config_apt_no_proxy: Optional[str]
65 vca_model_config_automatically_retry_hooks: Optional[bool]
66 vca_model_config_backup_dir: Optional[str]
67 vca_model_config_cloudinit_userdata: Optional[str]
68 vca_model_config_container_image_metadata_url: Optional[str]
69 vca_model_config_container_image_stream: Optional[str]
70 vca_model_config_container_inherit_properties: Optional[str]
71 vca_model_config_container_networking_method: Optional[str]
72 vca_model_config_default_series: Optional[str]
73 vca_model_config_default_space: Optional[str]
74 vca_model_config_development: Optional[bool]
75 vca_model_config_disable_network_management: Optional[bool]
76 vca_model_config_egress_subnets: Optional[str]
77 vca_model_config_enable_os_refresh_update: Optional[bool]
78 vca_model_config_enable_os_upgrade: Optional[bool]
79 vca_model_config_fan_config: Optional[str]
80 vca_model_config_firewall_mode: Optional[str]
81 vca_model_config_ftp_proxy: Optional[str]
82 vca_model_config_http_proxy: Optional[str]
83 vca_model_config_https_proxy: Optional[str]
84 vca_model_config_ignore_machine_addresses: Optional[bool]
85 vca_model_config_image_metadata_url: Optional[str]
86 vca_model_config_image_stream: Optional[str]
87 vca_model_config_juju_ftp_proxy: Optional[str]
88 vca_model_config_juju_http_proxy: Optional[str]
89 vca_model_config_juju_https_proxy: Optional[str]
90 vca_model_config_juju_no_proxy: Optional[str]
91 vca_model_config_logforward_enabled: Optional[bool]
92 vca_model_config_logging_config: Optional[str]
93 vca_model_config_lxd_snap_channel: Optional[str]
94 vca_model_config_max_action_results_age: Optional[str]
95 vca_model_config_max_action_results_size: Optional[str]
96 vca_model_config_max_status_history_age: Optional[str]
97 vca_model_config_max_status_history_size: Optional[str]
98 vca_model_config_net_bond_reconfigure_delay: Optional[str]
99 vca_model_config_no_proxy: Optional[str]
100 vca_model_config_provisioner_harvest_mode: Optional[str]
101 vca_model_config_proxy_ssh: Optional[bool]
102 vca_model_config_snap_http_proxy: Optional[str]
103 vca_model_config_snap_https_proxy: Optional[str]
104 vca_model_config_snap_store_assertions: Optional[str]
105 vca_model_config_snap_store_proxy: Optional[str]
106 vca_model_config_snap_store_proxy_url: Optional[str]
107 vca_model_config_ssl_hostname_verification: Optional[bool]
108 vca_model_config_test_mode: Optional[bool]
109 vca_model_config_transmit_vendor_metrics: Optional[bool]
110 vca_model_config_update_status_hook_interval: Optional[str]
111 vca_stablerepourl: Optional[str]
112
113 @validator("log_level")
114 def validate_log_level(cls, v):
115 if v not in {"INFO", "DEBUG"}:
116 raise ValueError("value must be INFO or DEBUG")
117 return v
118
119 @validator("mongodb_uri")
120 def validate_mongodb_uri(cls, v):
121 if v and not v.startswith("mongodb://"):
122 raise ValueError("mongodb_uri is not properly formed")
123 return v
124
125
126 class LcmCharm(CharmedOsmBase):
127 def __init__(self, *args) -> NoReturn:
128 super().__init__(*args, oci_image="image")
129
130 self.kafka_client = KafkaClient(self, "kafka")
131 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
132 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
133
134 self.mongodb_client = MongoClient(self, "mongodb")
135 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
136 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
137
138 self.ro_client = HttpClient(self, "ro")
139 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
140 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
141
142 def _check_missing_dependencies(self, config: ConfigModel):
143 missing_relations = []
144
145 if self.kafka_client.is_missing_data_in_unit():
146 missing_relations.append("kafka")
147 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
148 missing_relations.append("mongodb")
149 if self.ro_client.is_missing_data_in_app():
150 missing_relations.append("ro")
151
152 if missing_relations:
153 raise RelationsMissing(missing_relations)
154
155 def build_pod_spec(self, image_info):
156 # Validate config
157 config = ConfigModel(**dict(self.config))
158
159 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
160 raise Exception("Mongodb data cannot be provided via config and relation")
161
162 # Check relations
163 self._check_missing_dependencies(config)
164
165 # Create Builder for the PodSpec
166 pod_spec_builder = PodSpecV3Builder()
167
168 # Build Container
169 container_builder = ContainerV3Builder(self.app.name, image_info)
170 container_builder.add_port(name=self.app.name, port=PORT)
171 container_builder.add_envs(
172 {
173 # General configuration
174 "ALLOW_ANONYMOUS_LOGIN": "yes",
175 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
176 # RO configuration
177 "OSMLCM_RO_HOST": self.ro_client.host,
178 "OSMLCM_RO_PORT": self.ro_client.port,
179 "OSMLCM_RO_TENANT": "osm",
180 # Kafka configuration
181 "OSMLCM_MESSAGE_DRIVER": "kafka",
182 "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
183 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
184 # Database configuration
185 "OSMLCM_DATABASE_DRIVER": "mongo",
186 "OSMLCM_DATABASE_URI": config.mongodb_uri
187 or self.mongodb_client.connection_string,
188 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
189 # Storage configuration
190 "OSMLCM_STORAGE_DRIVER": "mongo",
191 "OSMLCM_STORAGE_PATH": "/app/storage",
192 "OSMLCM_STORAGE_COLLECTION": "files",
193 "OSMLCM_STORAGE_URI": config.mongodb_uri
194 or self.mongodb_client.connection_string,
195 "OSMLCM_VCA_STABLEREPOURL": config.vca_stablerepourl,
196 }
197 )
198 if config.vca_host:
199 container_builder.add_envs(
200 {
201 # VCA configuration
202 "OSMLCM_VCA_HOST": config.vca_host,
203 "OSMLCM_VCA_PORT": config.vca_port,
204 "OSMLCM_VCA_USER": config.vca_user,
205 "OSMLCM_VCA_PUBKEY": config.vca_pubkey,
206 "OSMLCM_VCA_SECRET": config.vca_secret,
207 "OSMLCM_VCA_CACERT": config.vca_cacert,
208 "OSMLCM_VCA_CLOUD": config.vca_cloud,
209 "OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud,
210 }
211 )
212 if config.vca_apiproxy:
213 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
214
215 model_config_envs = {
216 f"OSMLCM_{k.upper()}": v
217 for k, v in self.config.items()
218 if k.startswith("vca_model_config")
219 }
220 if model_config_envs:
221 container_builder.add_envs(model_config_envs)
222 container = container_builder.build()
223
224 # Add container to pod spec
225 pod_spec_builder.add_container(container)
226
227 return pod_spec_builder.build()
228
229
230 if __name__ == "__main__":
231 main(LcmCharm)
232
233
234 # class ConfigurePodEvent(EventBase):
235 # """Configure Pod event"""
236
237 # pass
238
239
240 # class LcmEvents(CharmEvents):
241 # """LCM Events"""
242
243 # configure_pod = EventSource(ConfigurePodEvent)
244
245
246 # class LcmCharm(CharmBase):
247 # """LCM Charm."""
248
249 # state = StoredState()
250 # on = LcmEvents()
251
252 # def __init__(self, *args) -> NoReturn:
253 # """LCM Charm constructor."""
254 # super().__init__(*args)
255
256 # # Internal state initialization
257 # self.state.set_default(pod_spec=None)
258
259 # # Message bus data initialization
260 # self.state.set_default(message_host=None)
261 # self.state.set_default(message_port=None)
262
263 # # Database data initialization
264 # self.state.set_default(database_uri=None)
265
266 # # RO data initialization
267 # self.state.set_default(ro_host=None)
268 # self.state.set_default(ro_port=None)
269
270 # self.port = LCM_PORT
271 # self.image = OCIImageResource(self, "image")
272
273 # # Registering regular events
274 # self.framework.observe(self.on.start, self.configure_pod)
275 # self.framework.observe(self.on.config_changed, self.configure_pod)
276 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
277
278 # # Registering custom internal events
279 # self.framework.observe(self.on.configure_pod, self.configure_pod)
280
281 # # Registering required relation events
282 # self.framework.observe(
283 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
284 # )
285 # self.framework.observe(
286 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
287 # )
288 # self.framework.observe(
289 # self.on.ro_relation_changed, self._on_ro_relation_changed
290 # )
291
292 # # Registering required relation broken events
293 # self.framework.observe(
294 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
295 # )
296 # self.framework.observe(
297 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
298 # )
299 # self.framework.observe(
300 # self.on.ro_relation_broken, self._on_ro_relation_broken
301 # )
302
303 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
304 # """Reads information about the kafka relation.
305
306 # Args:
307 # event (EventBase): Kafka relation event.
308 # """
309 # message_host = event.relation.data[event.unit].get("host")
310 # message_port = event.relation.data[event.unit].get("port")
311
312 # if (
313 # message_host
314 # and message_port
315 # and (
316 # self.state.message_host != message_host
317 # or self.state.message_port != message_port
318 # )
319 # ):
320 # self.state.message_host = message_host
321 # self.state.message_port = message_port
322 # self.on.configure_pod.emit()
323
324 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
325 # """Clears data from kafka relation.
326
327 # Args:
328 # event (EventBase): Kafka relation event.
329 # """
330 # self.state.message_host = None
331 # self.state.message_port = None
332 # self.on.configure_pod.emit()
333
334 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
335 # """Reads information about the DB relation.
336
337 # Args:
338 # event (EventBase): DB relation event.
339 # """
340 # database_uri = event.relation.data[event.unit].get("connection_string")
341
342 # if database_uri and self.state.database_uri != database_uri:
343 # self.state.database_uri = database_uri
344 # self.on.configure_pod.emit()
345
346 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
347 # """Clears data from mongodb relation.
348
349 # Args:
350 # event (EventBase): DB relation event.
351 # """
352 # self.state.database_uri = None
353 # self.on.configure_pod.emit()
354
355 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
356 # """Reads information about the RO relation.
357
358 # Args:
359 # event (EventBase): Keystone relation event.
360 # """
361 # ro_host = event.relation.data[event.unit].get("host")
362 # ro_port = event.relation.data[event.unit].get("port")
363
364 # if (
365 # ro_host
366 # and ro_port
367 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
368 # ):
369 # self.state.ro_host = ro_host
370 # self.state.ro_port = ro_port
371 # self.on.configure_pod.emit()
372
373 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
374 # """Clears data from ro relation.
375
376 # Args:
377 # event (EventBase): Keystone relation event.
378 # """
379 # self.state.ro_host = None
380 # self.state.ro_port = None
381 # self.on.configure_pod.emit()
382
383 # def _missing_relations(self) -> str:
384 # """Checks if there missing relations.
385
386 # Returns:
387 # str: string with missing relations
388 # """
389 # data_status = {
390 # "kafka": self.state.message_host,
391 # "mongodb": self.state.database_uri,
392 # "ro": self.state.ro_host,
393 # }
394
395 # missing_relations = [k for k, v in data_status.items() if not v]
396
397 # return ", ".join(missing_relations)
398
399 # @property
400 # def relation_state(self) -> Dict[str, Any]:
401 # """Collects relation state configuration for pod spec assembly.
402
403 # Returns:
404 # Dict[str, Any]: relation state information.
405 # """
406 # relation_state = {
407 # "message_host": self.state.message_host,
408 # "message_port": self.state.message_port,
409 # "database_uri": self.state.database_uri,
410 # "ro_host": self.state.ro_host,
411 # "ro_port": self.state.ro_port,
412 # }
413
414 # return relation_state
415
416 # def configure_pod(self, event: EventBase) -> NoReturn:
417 # """Assemble the pod spec and apply it, if possible.
418
419 # Args:
420 # event (EventBase): Hook or Relation event that started the
421 # function.
422 # """
423 # if missing := self._missing_relations():
424 # self.unit.status = BlockedStatus(
425 # "Waiting for {0} relation{1}".format(
426 # missing, "s" if "," in missing else ""
427 # )
428 # )
429 # return
430
431 # if not self.unit.is_leader():
432 # self.unit.status = ActiveStatus("ready")
433 # return
434
435 # self.unit.status = MaintenanceStatus("Assembling pod spec")
436
437 # # Fetch image information
438 # try:
439 # self.unit.status = MaintenanceStatus("Fetching image information")
440 # image_info = self.image.fetch()
441 # except OCIImageResourceError:
442 # self.unit.status = BlockedStatus("Error fetching image information")
443 # return
444
445 # try:
446 # pod_spec = make_pod_spec(
447 # image_info,
448 # self.model.config,
449 # self.relation_state,
450 # self.model.app.name,
451 # self.port,
452 # )
453 # except ValueError as exc:
454 # logger.exception("Config/Relation data validation error")
455 # self.unit.status = BlockedStatus(str(exc))
456 # return
457
458 # if self.state.pod_spec != pod_spec:
459 # self.model.pod.set_spec(pod_spec)
460 # self.state.pod_spec = pod_spec
461
462 # self.unit.status = ActiveStatus("ready")
463
464
465 # if __name__ == "__main__":
466 # main(LcmCharm)