Fix bug 1766: update to latest charm revisions (Charmed installer)
[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, PodRestartPolicy, 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 vca_helm_ca_certs: Optional[str]
113 image_pull_policy: str
114 debug_mode: bool
115 security_context: bool
116
117 @validator("log_level")
118 def validate_log_level(cls, v):
119 if v not in {"INFO", "DEBUG"}:
120 raise ValueError("value must be INFO or DEBUG")
121 return v
122
123 @validator("mongodb_uri")
124 def validate_mongodb_uri(cls, v):
125 if v and not v.startswith("mongodb://"):
126 raise ValueError("mongodb_uri is not properly formed")
127 return v
128
129 @validator("image_pull_policy")
130 def validate_image_pull_policy(cls, v):
131 values = {
132 "always": "Always",
133 "ifnotpresent": "IfNotPresent",
134 "never": "Never",
135 }
136 v = v.lower()
137 if v not in values.keys():
138 raise ValueError("value must be always, ifnotpresent or never")
139 return values[v]
140
141
142 class LcmCharm(CharmedOsmBase):
143 def __init__(self, *args) -> NoReturn:
144 super().__init__(
145 *args,
146 oci_image="image",
147 debug_mode_config_key="debug_mode",
148 debug_pubkey_config_key="debug_pubkey",
149 vscode_workspace=VSCODE_WORKSPACE,
150 )
151 self.kafka_client = KafkaClient(self, "kafka")
152 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
153 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
154
155 self.mongodb_client = MongoClient(self, "mongodb")
156 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
157 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
158
159 self.ro_client = HttpClient(self, "ro")
160 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
161 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
162
163 def _check_missing_dependencies(self, config: ConfigModel):
164 missing_relations = []
165
166 if (
167 self.kafka_client.is_missing_data_in_unit()
168 and self.kafka_client.is_missing_data_in_app()
169 ):
170 missing_relations.append("kafka")
171 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
172 missing_relations.append("mongodb")
173 if self.ro_client.is_missing_data_in_app():
174 missing_relations.append("ro")
175
176 if missing_relations:
177 raise RelationsMissing(missing_relations)
178
179 def build_pod_spec(self, image_info):
180 # Validate config
181 config = ConfigModel(**dict(self.config))
182
183 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
184 raise Exception("Mongodb data cannot be provided via config and relation")
185
186 # Check relations
187 self._check_missing_dependencies(config)
188
189 security_context_enabled = (
190 config.security_context if not config.debug_mode else False
191 )
192
193 # Create Builder for the PodSpec
194 pod_spec_builder = PodSpecV3Builder(
195 enable_security_context=security_context_enabled
196 )
197
198 # Add secrets to the pod
199 lcm_secret_name = f"{self.app.name}-lcm-secret"
200 pod_spec_builder.add_secret(
201 lcm_secret_name,
202 {
203 "uri": config.mongodb_uri or self.mongodb_client.connection_string,
204 "commonkey": config.database_commonkey,
205 "helm_ca_certs": config.vca_helm_ca_certs,
206 },
207 )
208
209 # Build Container
210 container_builder = ContainerV3Builder(
211 self.app.name,
212 image_info,
213 config.image_pull_policy,
214 run_as_non_root=security_context_enabled,
215 )
216 container_builder.add_port(name=self.app.name, port=PORT)
217 container_builder.add_envs(
218 {
219 # General configuration
220 "ALLOW_ANONYMOUS_LOGIN": "yes",
221 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
222 # RO configuration
223 "OSMLCM_RO_HOST": self.ro_client.host,
224 "OSMLCM_RO_PORT": self.ro_client.port,
225 "OSMLCM_RO_TENANT": "osm",
226 # Kafka configuration
227 "OSMLCM_MESSAGE_DRIVER": "kafka",
228 "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
229 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
230 # Database configuration
231 "OSMLCM_DATABASE_DRIVER": "mongo",
232 # Storage configuration
233 "OSMLCM_STORAGE_DRIVER": "mongo",
234 "OSMLCM_STORAGE_PATH": "/app/storage",
235 "OSMLCM_STORAGE_COLLECTION": "files",
236 "OSMLCM_VCA_STABLEREPOURL": config.vca_stablerepourl,
237 }
238 )
239 container_builder.add_secret_envs(
240 secret_name=lcm_secret_name,
241 envs={
242 "OSMLCM_DATABASE_URI": "uri",
243 "OSMLCM_DATABASE_COMMONKEY": "commonkey",
244 "OSMLCM_STORAGE_URI": "uri",
245 "OSMLCM_VCA_HELM_CA_CERTS": "helm_ca_certs",
246 },
247 )
248 if config.vca_host:
249 vca_secret_name = f"{self.app.name}-vca-secret"
250 pod_spec_builder.add_secret(
251 vca_secret_name,
252 {
253 "host": config.vca_host,
254 "port": str(config.vca_port),
255 "user": config.vca_user,
256 "pubkey": config.vca_pubkey,
257 "secret": config.vca_secret,
258 "cacert": config.vca_cacert,
259 "cloud": config.vca_cloud,
260 "k8s_cloud": config.vca_k8s_cloud,
261 },
262 )
263 container_builder.add_secret_envs(
264 secret_name=vca_secret_name,
265 envs={
266 # VCA configuration
267 "OSMLCM_VCA_HOST": "host",
268 "OSMLCM_VCA_PORT": "port",
269 "OSMLCM_VCA_USER": "user",
270 "OSMLCM_VCA_PUBKEY": "pubkey",
271 "OSMLCM_VCA_SECRET": "secret",
272 "OSMLCM_VCA_CACERT": "cacert",
273 "OSMLCM_VCA_CLOUD": "cloud",
274 "OSMLCM_VCA_K8S_CLOUD": "k8s_cloud",
275 },
276 )
277 if config.vca_apiproxy:
278 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
279
280 model_config_envs = {
281 f"OSMLCM_{k.upper()}": v
282 for k, v in self.config.items()
283 if k.startswith("vca_model_config")
284 }
285 if model_config_envs:
286 container_builder.add_envs(model_config_envs)
287 container = container_builder.build()
288
289 # Add container to pod spec
290 pod_spec_builder.add_container(container)
291
292 # Add restart policy
293 restart_policy = PodRestartPolicy()
294 restart_policy.add_secrets()
295 pod_spec_builder.set_restart_policy(restart_policy)
296
297 return pod_spec_builder.build()
298
299
300 VSCODE_WORKSPACE = {
301 "folders": [
302 {"path": "/usr/lib/python3/dist-packages/osm_lcm"},
303 {"path": "/usr/lib/python3/dist-packages/n2vc"},
304 {"path": "/usr/lib/python3/dist-packages/osm_common"},
305 ],
306 "settings": {},
307 "launch": {
308 "version": "0.2.0",
309 "configurations": [
310 {
311 "name": "LCM",
312 "type": "python",
313 "request": "launch",
314 "module": "osm_lcm.lcm",
315 "justMyCode": False,
316 }
317 ],
318 },
319 }
320
321
322 if __name__ == "__main__":
323 main(LcmCharm)
324
325
326 # class ConfigurePodEvent(EventBase):
327 # """Configure Pod event"""
328
329 # pass
330
331
332 # class LcmEvents(CharmEvents):
333 # """LCM Events"""
334
335 # configure_pod = EventSource(ConfigurePodEvent)
336
337
338 # class LcmCharm(CharmBase):
339 # """LCM Charm."""
340
341 # state = StoredState()
342 # on = LcmEvents()
343
344 # def __init__(self, *args) -> NoReturn:
345 # """LCM Charm constructor."""
346 # super().__init__(*args)
347
348 # # Internal state initialization
349 # self.state.set_default(pod_spec=None)
350
351 # # Message bus data initialization
352 # self.state.set_default(message_host=None)
353 # self.state.set_default(message_port=None)
354
355 # # Database data initialization
356 # self.state.set_default(database_uri=None)
357
358 # # RO data initialization
359 # self.state.set_default(ro_host=None)
360 # self.state.set_default(ro_port=None)
361
362 # self.port = LCM_PORT
363 # self.image = OCIImageResource(self, "image")
364
365 # # Registering regular events
366 # self.framework.observe(self.on.start, self.configure_pod)
367 # self.framework.observe(self.on.config_changed, self.configure_pod)
368 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
369
370 # # Registering custom internal events
371 # self.framework.observe(self.on.configure_pod, self.configure_pod)
372
373 # # Registering required relation events
374 # self.framework.observe(
375 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
376 # )
377 # self.framework.observe(
378 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
379 # )
380 # self.framework.observe(
381 # self.on.ro_relation_changed, self._on_ro_relation_changed
382 # )
383
384 # # Registering required relation broken events
385 # self.framework.observe(
386 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
387 # )
388 # self.framework.observe(
389 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
390 # )
391 # self.framework.observe(
392 # self.on.ro_relation_broken, self._on_ro_relation_broken
393 # )
394
395 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
396 # """Reads information about the kafka relation.
397
398 # Args:
399 # event (EventBase): Kafka relation event.
400 # """
401 # message_host = event.relation.data[event.unit].get("host")
402 # message_port = event.relation.data[event.unit].get("port")
403
404 # if (
405 # message_host
406 # and message_port
407 # and (
408 # self.state.message_host != message_host
409 # or self.state.message_port != message_port
410 # )
411 # ):
412 # self.state.message_host = message_host
413 # self.state.message_port = message_port
414 # self.on.configure_pod.emit()
415
416 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
417 # """Clears data from kafka relation.
418
419 # Args:
420 # event (EventBase): Kafka relation event.
421 # """
422 # self.state.message_host = None
423 # self.state.message_port = None
424 # self.on.configure_pod.emit()
425
426 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
427 # """Reads information about the DB relation.
428
429 # Args:
430 # event (EventBase): DB relation event.
431 # """
432 # database_uri = event.relation.data[event.unit].get("connection_string")
433
434 # if database_uri and self.state.database_uri != database_uri:
435 # self.state.database_uri = database_uri
436 # self.on.configure_pod.emit()
437
438 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
439 # """Clears data from mongodb relation.
440
441 # Args:
442 # event (EventBase): DB relation event.
443 # """
444 # self.state.database_uri = None
445 # self.on.configure_pod.emit()
446
447 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
448 # """Reads information about the RO relation.
449
450 # Args:
451 # event (EventBase): Keystone relation event.
452 # """
453 # ro_host = event.relation.data[event.unit].get("host")
454 # ro_port = event.relation.data[event.unit].get("port")
455
456 # if (
457 # ro_host
458 # and ro_port
459 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
460 # ):
461 # self.state.ro_host = ro_host
462 # self.state.ro_port = ro_port
463 # self.on.configure_pod.emit()
464
465 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
466 # """Clears data from ro relation.
467
468 # Args:
469 # event (EventBase): Keystone relation event.
470 # """
471 # self.state.ro_host = None
472 # self.state.ro_port = None
473 # self.on.configure_pod.emit()
474
475 # def _missing_relations(self) -> str:
476 # """Checks if there missing relations.
477
478 # Returns:
479 # str: string with missing relations
480 # """
481 # data_status = {
482 # "kafka": self.state.message_host,
483 # "mongodb": self.state.database_uri,
484 # "ro": self.state.ro_host,
485 # }
486
487 # missing_relations = [k for k, v in data_status.items() if not v]
488
489 # return ", ".join(missing_relations)
490
491 # @property
492 # def relation_state(self) -> Dict[str, Any]:
493 # """Collects relation state configuration for pod spec assembly.
494
495 # Returns:
496 # Dict[str, Any]: relation state information.
497 # """
498 # relation_state = {
499 # "message_host": self.state.message_host,
500 # "message_port": self.state.message_port,
501 # "database_uri": self.state.database_uri,
502 # "ro_host": self.state.ro_host,
503 # "ro_port": self.state.ro_port,
504 # }
505
506 # return relation_state
507
508 # def configure_pod(self, event: EventBase) -> NoReturn:
509 # """Assemble the pod spec and apply it, if possible.
510
511 # Args:
512 # event (EventBase): Hook or Relation event that started the
513 # function.
514 # """
515 # if missing := self._missing_relations():
516 # self.unit.status = BlockedStatus(
517 # "Waiting for {0} relation{1}".format(
518 # missing, "s" if "," in missing else ""
519 # )
520 # )
521 # return
522
523 # if not self.unit.is_leader():
524 # self.unit.status = ActiveStatus("ready")
525 # return
526
527 # self.unit.status = MaintenanceStatus("Assembling pod spec")
528
529 # # Fetch image information
530 # try:
531 # self.unit.status = MaintenanceStatus("Fetching image information")
532 # image_info = self.image.fetch()
533 # except OCIImageResourceError:
534 # self.unit.status = BlockedStatus("Error fetching image information")
535 # return
536
537 # try:
538 # pod_spec = make_pod_spec(
539 # image_info,
540 # self.model.config,
541 # self.relation_state,
542 # self.model.app.name,
543 # self.port,
544 # )
545 # except ValueError as exc:
546 # logger.exception("Config/Relation data validation error")
547 # self.unit.status = BlockedStatus(str(exc))
548 # return
549
550 # if self.state.pod_spec != pod_spec:
551 # self.model.pod.set_spec(pod_spec)
552 # self.state.pod_spec = pod_spec
553
554 # self.unit.status = ActiveStatus("ready")
555
556
557 # if __name__ == "__main__":
558 # main(LcmCharm)