2 # Copyright 2021 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # pylint: disable=E0213
27 from typing
import NoReturn
, Optional
30 from charms
.kafka_k8s
.v0
.kafka
import KafkaEvents
, KafkaRequires
31 from ops
.main
import main
32 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
33 from opslib
.osm
.interfaces
.http
import HttpClient
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
39 logger
= logging
.getLogger(__name__
)
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]
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
115 security_context
: bool
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")
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")
129 @validator("image_pull_policy")
130 def validate_image_pull_policy(cls
, v
):
133 "ifnotpresent": "IfNotPresent",
137 if v
not in values
.keys():
138 raise ValueError("value must be always, ifnotpresent or never")
142 class LcmCharm(CharmedOsmBase
):
146 def __init__(self
, *args
) -> NoReturn
:
150 vscode_workspace
=VSCODE_WORKSPACE
,
152 if self
.config
.get("debug_mode"):
153 self
.enable_debug_mode(
154 pubkey
=self
.config
.get("debug_pubkey"),
157 "hostpath": self
.config
.get("debug_lcm_local_path"),
158 "container-path": "/usr/lib/python3/dist-packages/osm_lcm",
161 "hostpath": self
.config
.get("debug_n2vc_local_path"),
162 "container-path": "/usr/lib/python3/dist-packages/n2vc",
165 "hostpath": self
.config
.get("debug_common_local_path"),
166 "container-path": "/usr/lib/python3/dist-packages/osm_common",
170 self
.kafka
= KafkaRequires(self
)
171 self
.framework
.observe(self
.on
.kafka_available
, self
.configure_pod
)
172 self
.framework
.observe(self
.on
.kafka_broken
, self
.configure_pod
)
174 self
.mongodb_client
= MongoClient(self
, "mongodb")
175 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
176 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
178 self
.ro_client
= HttpClient(self
, "ro")
179 self
.framework
.observe(self
.on
["ro"].relation_changed
, self
.configure_pod
)
180 self
.framework
.observe(self
.on
["ro"].relation_broken
, self
.configure_pod
)
182 def _check_missing_dependencies(self
, config
: ConfigModel
):
183 missing_relations
= []
185 if not self
.kafka
.host
or not self
.kafka
.port
:
186 missing_relations
.append("kafka")
187 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
188 missing_relations
.append("mongodb")
189 if self
.ro_client
.is_missing_data_in_app():
190 missing_relations
.append("ro")
192 if missing_relations
:
193 raise RelationsMissing(missing_relations
)
195 def build_pod_spec(self
, image_info
):
197 config
= ConfigModel(**dict(self
.config
))
199 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
200 raise Exception("Mongodb data cannot be provided via config and relation")
203 self
._check
_missing
_dependencies
(config
)
205 security_context_enabled
= (
206 config
.security_context
if not config
.debug_mode
else False
209 # Create Builder for the PodSpec
210 pod_spec_builder
= PodSpecV3Builder(
211 enable_security_context
=security_context_enabled
214 # Add secrets to the pod
215 mongodb_secret_name
= f
"{self.app.name}-mongodb-secret"
216 pod_spec_builder
.add_secret(
219 "uri": config
.mongodb_uri
or self
.mongodb_client
.connection_string
,
220 "commonkey": config
.database_commonkey
,
221 "helm_ca_certs": config
.vca_helm_ca_certs
,
226 container_builder
= ContainerV3Builder(
229 config
.image_pull_policy
,
230 run_as_non_root
=security_context_enabled
,
232 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
233 container_builder
.add_envs(
235 # General configuration
236 "ALLOW_ANONYMOUS_LOGIN": "yes",
237 "OSMLCM_GLOBAL_LOGLEVEL": config
.log_level
,
239 "OSMLCM_RO_HOST": self
.ro_client
.host
,
240 "OSMLCM_RO_PORT": self
.ro_client
.port
,
241 "OSMLCM_RO_TENANT": "osm",
242 # Kafka configuration
243 "OSMLCM_MESSAGE_DRIVER": "kafka",
244 "OSMLCM_MESSAGE_HOST": self
.kafka
.host
,
245 "OSMLCM_MESSAGE_PORT": self
.kafka
.port
,
246 # Database configuration
247 "OSMLCM_DATABASE_DRIVER": "mongo",
248 # Storage configuration
249 "OSMLCM_STORAGE_DRIVER": "mongo",
250 "OSMLCM_STORAGE_PATH": "/app/storage",
251 "OSMLCM_STORAGE_COLLECTION": "files",
252 "OSMLCM_VCA_STABLEREPOURL": config
.vca_stablerepourl
,
255 container_builder
.add_secret_envs(
256 secret_name
=mongodb_secret_name
,
258 "OSMLCM_DATABASE_URI": "uri",
259 "OSMLCM_DATABASE_COMMONKEY": "commonkey",
260 "OSMLCM_STORAGE_URI": "uri",
261 "OSMLCM_VCA_HELM_CA_CERTS": "helm_ca_certs",
265 vca_secret_name
= f
"{self.app.name}-vca-secret"
266 pod_spec_builder
.add_secret(
269 "host": config
.vca_host
,
270 "port": str(config
.vca_port
),
271 "user": config
.vca_user
,
272 "pubkey": config
.vca_pubkey
,
273 "secret": config
.vca_secret
,
274 "cacert": config
.vca_cacert
,
275 "cloud": config
.vca_cloud
,
276 "k8s_cloud": config
.vca_k8s_cloud
,
279 container_builder
.add_secret_envs(
280 secret_name
=vca_secret_name
,
283 "OSMLCM_VCA_HOST": "host",
284 "OSMLCM_VCA_PORT": "port",
285 "OSMLCM_VCA_USER": "user",
286 "OSMLCM_VCA_PUBKEY": "pubkey",
287 "OSMLCM_VCA_SECRET": "secret",
288 "OSMLCM_VCA_CACERT": "cacert",
289 "OSMLCM_VCA_CLOUD": "cloud",
290 "OSMLCM_VCA_K8S_CLOUD": "k8s_cloud",
293 if config
.vca_apiproxy
:
294 container_builder
.add_env("OSMLCM_VCA_APIPROXY", config
.vca_apiproxy
)
296 model_config_envs
= {
297 f
"OSMLCM_{k.upper()}": v
298 for k
, v
in self
.config
.items()
299 if k
.startswith("vca_model_config")
301 if model_config_envs
:
302 container_builder
.add_envs(model_config_envs
)
303 container
= container_builder
.build()
305 # Add container to pod spec
306 pod_spec_builder
.add_container(container
)
309 restart_policy
= PodRestartPolicy()
310 restart_policy
.add_secrets()
311 pod_spec_builder
.set_restart_policy(restart_policy
)
313 return pod_spec_builder
.build()
318 {"path": "/usr/lib/python3/dist-packages/osm_lcm"},
319 {"path": "/usr/lib/python3/dist-packages/n2vc"},
320 {"path": "/usr/lib/python3/dist-packages/osm_common"},
330 "module": "osm_lcm.lcm",
338 if __name__
== "__main__":
342 # class ConfigurePodEvent(EventBase):
343 # """Configure Pod event"""
348 # class LcmEvents(CharmEvents):
351 # configure_pod = EventSource(ConfigurePodEvent)
354 # class LcmCharm(CharmBase):
357 # state = StoredState()
360 # def __init__(self, *args) -> NoReturn:
361 # """LCM Charm constructor."""
362 # super().__init__(*args)
364 # # Internal state initialization
365 # self.state.set_default(pod_spec=None)
367 # # Message bus data initialization
368 # self.state.set_default(message_host=None)
369 # self.state.set_default(message_port=None)
371 # # Database data initialization
372 # self.state.set_default(database_uri=None)
374 # # RO data initialization
375 # self.state.set_default(ro_host=None)
376 # self.state.set_default(ro_port=None)
378 # self.port = LCM_PORT
379 # self.image = OCIImageResource(self, "image")
381 # # Registering regular events
382 # self.framework.observe(self.on.start, self.configure_pod)
383 # self.framework.observe(self.on.config_changed, self.configure_pod)
384 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
386 # # Registering custom internal events
387 # self.framework.observe(self.on.configure_pod, self.configure_pod)
389 # # Registering required relation events
390 # self.framework.observe(
391 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
393 # self.framework.observe(
394 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
396 # self.framework.observe(
397 # self.on.ro_relation_changed, self._on_ro_relation_changed
400 # # Registering required relation broken events
401 # self.framework.observe(
402 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
404 # self.framework.observe(
405 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
407 # self.framework.observe(
408 # self.on.ro_relation_broken, self._on_ro_relation_broken
411 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
412 # """Reads information about the kafka relation.
415 # event (EventBase): Kafka relation event.
417 # message_host = event.relation.data[event.unit].get("host")
418 # message_port = event.relation.data[event.unit].get("port")
424 # self.state.message_host != message_host
425 # or self.state.message_port != message_port
428 # self.state.message_host = message_host
429 # self.state.message_port = message_port
430 # self.on.configure_pod.emit()
432 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
433 # """Clears data from kafka relation.
436 # event (EventBase): Kafka relation event.
438 # self.state.message_host = None
439 # self.state.message_port = None
440 # self.on.configure_pod.emit()
442 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
443 # """Reads information about the DB relation.
446 # event (EventBase): DB relation event.
448 # database_uri = event.relation.data[event.unit].get("connection_string")
450 # if database_uri and self.state.database_uri != database_uri:
451 # self.state.database_uri = database_uri
452 # self.on.configure_pod.emit()
454 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
455 # """Clears data from mongodb relation.
458 # event (EventBase): DB relation event.
460 # self.state.database_uri = None
461 # self.on.configure_pod.emit()
463 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
464 # """Reads information about the RO relation.
467 # event (EventBase): Keystone relation event.
469 # ro_host = event.relation.data[event.unit].get("host")
470 # ro_port = event.relation.data[event.unit].get("port")
475 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
477 # self.state.ro_host = ro_host
478 # self.state.ro_port = ro_port
479 # self.on.configure_pod.emit()
481 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
482 # """Clears data from ro relation.
485 # event (EventBase): Keystone relation event.
487 # self.state.ro_host = None
488 # self.state.ro_port = None
489 # self.on.configure_pod.emit()
491 # def _missing_relations(self) -> str:
492 # """Checks if there missing relations.
495 # str: string with missing relations
498 # "kafka": self.state.message_host,
499 # "mongodb": self.state.database_uri,
500 # "ro": self.state.ro_host,
503 # missing_relations = [k for k, v in data_status.items() if not v]
505 # return ", ".join(missing_relations)
508 # def relation_state(self) -> Dict[str, Any]:
509 # """Collects relation state configuration for pod spec assembly.
512 # Dict[str, Any]: relation state information.
515 # "message_host": self.state.message_host,
516 # "message_port": self.state.message_port,
517 # "database_uri": self.state.database_uri,
518 # "ro_host": self.state.ro_host,
519 # "ro_port": self.state.ro_port,
522 # return relation_state
524 # def configure_pod(self, event: EventBase) -> NoReturn:
525 # """Assemble the pod spec and apply it, if possible.
528 # event (EventBase): Hook or Relation event that started the
531 # if missing := self._missing_relations():
532 # self.unit.status = BlockedStatus(
533 # "Waiting for {0} relation{1}".format(
534 # missing, "s" if "," in missing else ""
539 # if not self.unit.is_leader():
540 # self.unit.status = ActiveStatus("ready")
543 # self.unit.status = MaintenanceStatus("Assembling pod spec")
545 # # Fetch image information
547 # self.unit.status = MaintenanceStatus("Fetching image information")
548 # image_info = self.image.fetch()
549 # except OCIImageResourceError:
550 # self.unit.status = BlockedStatus("Error fetching image information")
554 # pod_spec = make_pod_spec(
557 # self.relation_state,
558 # self.model.app.name,
561 # except ValueError as exc:
562 # logger.exception("Config/Relation data validation error")
563 # self.unit.status = BlockedStatus(str(exc))
566 # if self.state.pod_spec != pod_spec:
567 # self.model.pod.set_spec(pod_spec)
568 # self.state.pod_spec = pod_spec
570 # self.unit.status = ActiveStatus("ready")
573 # if __name__ == "__main__":