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
):
145 def __init__(self
, *args
) -> NoReturn
:
149 vscode_workspace
=VSCODE_WORKSPACE
,
151 if self
.config
.get("debug_mode"):
152 self
.enable_debug_mode(
153 pubkey
=self
.config
.get("debug_pubkey"),
156 "hostpath": self
.config
.get("debug_lcm_local_path"),
157 "container-path": "/usr/lib/python3/dist-packages/osm_lcm",
160 "hostpath": self
.config
.get("debug_n2vc_local_path"),
161 "container-path": "/usr/lib/python3/dist-packages/n2vc",
164 "hostpath": self
.config
.get("debug_common_local_path"),
165 "container-path": "/usr/lib/python3/dist-packages/osm_common",
169 self
.kafka
= KafkaRequires(self
)
170 self
.framework
.observe(self
.on
.kafka_available
, self
.configure_pod
)
171 self
.framework
.observe(self
.on
.kafka_broken
, self
.configure_pod
)
173 self
.mongodb_client
= MongoClient(self
, "mongodb")
174 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
175 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
177 self
.ro_client
= HttpClient(self
, "ro")
178 self
.framework
.observe(self
.on
["ro"].relation_changed
, self
.configure_pod
)
179 self
.framework
.observe(self
.on
["ro"].relation_broken
, self
.configure_pod
)
181 def _check_missing_dependencies(self
, config
: ConfigModel
):
182 missing_relations
= []
184 if not self
.kafka
.host
or not self
.kafka
.port
:
185 missing_relations
.append("kafka")
186 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
187 missing_relations
.append("mongodb")
188 if self
.ro_client
.is_missing_data_in_app():
189 missing_relations
.append("ro")
191 if missing_relations
:
192 raise RelationsMissing(missing_relations
)
194 def build_pod_spec(self
, image_info
):
196 config
= ConfigModel(**dict(self
.config
))
198 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
199 raise Exception("Mongodb data cannot be provided via config and relation")
202 self
._check
_missing
_dependencies
(config
)
204 security_context_enabled
= (
205 config
.security_context
if not config
.debug_mode
else False
208 # Create Builder for the PodSpec
209 pod_spec_builder
= PodSpecV3Builder(
210 enable_security_context
=security_context_enabled
213 # Add secrets to the pod
214 mongodb_secret_name
= f
"{self.app.name}-mongodb-secret"
215 pod_spec_builder
.add_secret(
218 "uri": config
.mongodb_uri
or self
.mongodb_client
.connection_string
,
219 "commonkey": config
.database_commonkey
,
220 "helm_ca_certs": config
.vca_helm_ca_certs
,
225 container_builder
= ContainerV3Builder(
228 config
.image_pull_policy
,
229 run_as_non_root
=security_context_enabled
,
231 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
232 container_builder
.add_envs(
234 # General configuration
235 "ALLOW_ANONYMOUS_LOGIN": "yes",
236 "OSMLCM_GLOBAL_LOGLEVEL": config
.log_level
,
238 "OSMLCM_RO_HOST": self
.ro_client
.host
,
239 "OSMLCM_RO_PORT": self
.ro_client
.port
,
240 "OSMLCM_RO_TENANT": "osm",
241 # Kafka configuration
242 "OSMLCM_MESSAGE_DRIVER": "kafka",
243 "OSMLCM_MESSAGE_HOST": self
.kafka
.host
,
244 "OSMLCM_MESSAGE_PORT": self
.kafka
.port
,
245 # Database configuration
246 "OSMLCM_DATABASE_DRIVER": "mongo",
247 # Storage configuration
248 "OSMLCM_STORAGE_DRIVER": "mongo",
249 "OSMLCM_STORAGE_PATH": "/app/storage",
250 "OSMLCM_STORAGE_COLLECTION": "files",
251 "OSMLCM_VCA_STABLEREPOURL": config
.vca_stablerepourl
,
254 container_builder
.add_secret_envs(
255 secret_name
=mongodb_secret_name
,
257 "OSMLCM_DATABASE_URI": "uri",
258 "OSMLCM_DATABASE_COMMONKEY": "commonkey",
259 "OSMLCM_STORAGE_URI": "uri",
260 "OSMLCM_VCA_HELM_CA_CERTS": "helm_ca_certs",
264 vca_secret_name
= f
"{self.app.name}-vca-secret"
265 pod_spec_builder
.add_secret(
268 "host": config
.vca_host
,
269 "port": str(config
.vca_port
),
270 "user": config
.vca_user
,
271 "pubkey": config
.vca_pubkey
,
272 "secret": config
.vca_secret
,
273 "cacert": config
.vca_cacert
,
274 "cloud": config
.vca_cloud
,
275 "k8s_cloud": config
.vca_k8s_cloud
,
278 container_builder
.add_secret_envs(
279 secret_name
=vca_secret_name
,
282 "OSMLCM_VCA_HOST": "host",
283 "OSMLCM_VCA_PORT": "port",
284 "OSMLCM_VCA_USER": "user",
285 "OSMLCM_VCA_PUBKEY": "pubkey",
286 "OSMLCM_VCA_SECRET": "secret",
287 "OSMLCM_VCA_CACERT": "cacert",
288 "OSMLCM_VCA_CLOUD": "cloud",
289 "OSMLCM_VCA_K8S_CLOUD": "k8s_cloud",
292 if config
.vca_apiproxy
:
293 container_builder
.add_env("OSMLCM_VCA_APIPROXY", config
.vca_apiproxy
)
295 model_config_envs
= {
296 f
"OSMLCM_{k.upper()}": v
297 for k
, v
in self
.config
.items()
298 if k
.startswith("vca_model_config")
300 if model_config_envs
:
301 container_builder
.add_envs(model_config_envs
)
302 container
= container_builder
.build()
304 # Add container to pod spec
305 pod_spec_builder
.add_container(container
)
308 restart_policy
= PodRestartPolicy()
309 restart_policy
.add_secrets()
310 pod_spec_builder
.set_restart_policy(restart_policy
)
312 return pod_spec_builder
.build()
317 {"path": "/usr/lib/python3/dist-packages/osm_lcm"},
318 {"path": "/usr/lib/python3/dist-packages/n2vc"},
319 {"path": "/usr/lib/python3/dist-packages/osm_common"},
329 "module": "osm_lcm.lcm",
337 if __name__
== "__main__":
341 # class ConfigurePodEvent(EventBase):
342 # """Configure Pod event"""
347 # class LcmEvents(CharmEvents):
350 # configure_pod = EventSource(ConfigurePodEvent)
353 # class LcmCharm(CharmBase):
356 # state = StoredState()
359 # def __init__(self, *args) -> NoReturn:
360 # """LCM Charm constructor."""
361 # super().__init__(*args)
363 # # Internal state initialization
364 # self.state.set_default(pod_spec=None)
366 # # Message bus data initialization
367 # self.state.set_default(message_host=None)
368 # self.state.set_default(message_port=None)
370 # # Database data initialization
371 # self.state.set_default(database_uri=None)
373 # # RO data initialization
374 # self.state.set_default(ro_host=None)
375 # self.state.set_default(ro_port=None)
377 # self.port = LCM_PORT
378 # self.image = OCIImageResource(self, "image")
380 # # Registering regular events
381 # self.framework.observe(self.on.start, self.configure_pod)
382 # self.framework.observe(self.on.config_changed, self.configure_pod)
383 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
385 # # Registering custom internal events
386 # self.framework.observe(self.on.configure_pod, self.configure_pod)
388 # # Registering required relation events
389 # self.framework.observe(
390 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
392 # self.framework.observe(
393 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
395 # self.framework.observe(
396 # self.on.ro_relation_changed, self._on_ro_relation_changed
399 # # Registering required relation broken events
400 # self.framework.observe(
401 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
403 # self.framework.observe(
404 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
406 # self.framework.observe(
407 # self.on.ro_relation_broken, self._on_ro_relation_broken
410 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
411 # """Reads information about the kafka relation.
414 # event (EventBase): Kafka relation event.
416 # message_host = event.relation.data[event.unit].get("host")
417 # message_port = event.relation.data[event.unit].get("port")
423 # self.state.message_host != message_host
424 # or self.state.message_port != message_port
427 # self.state.message_host = message_host
428 # self.state.message_port = message_port
429 # self.on.configure_pod.emit()
431 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
432 # """Clears data from kafka relation.
435 # event (EventBase): Kafka relation event.
437 # self.state.message_host = None
438 # self.state.message_port = None
439 # self.on.configure_pod.emit()
441 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
442 # """Reads information about the DB relation.
445 # event (EventBase): DB relation event.
447 # database_uri = event.relation.data[event.unit].get("connection_string")
449 # if database_uri and self.state.database_uri != database_uri:
450 # self.state.database_uri = database_uri
451 # self.on.configure_pod.emit()
453 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
454 # """Clears data from mongodb relation.
457 # event (EventBase): DB relation event.
459 # self.state.database_uri = None
460 # self.on.configure_pod.emit()
462 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
463 # """Reads information about the RO relation.
466 # event (EventBase): Keystone relation event.
468 # ro_host = event.relation.data[event.unit].get("host")
469 # ro_port = event.relation.data[event.unit].get("port")
474 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
476 # self.state.ro_host = ro_host
477 # self.state.ro_port = ro_port
478 # self.on.configure_pod.emit()
480 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
481 # """Clears data from ro relation.
484 # event (EventBase): Keystone relation event.
486 # self.state.ro_host = None
487 # self.state.ro_port = None
488 # self.on.configure_pod.emit()
490 # def _missing_relations(self) -> str:
491 # """Checks if there missing relations.
494 # str: string with missing relations
497 # "kafka": self.state.message_host,
498 # "mongodb": self.state.database_uri,
499 # "ro": self.state.ro_host,
502 # missing_relations = [k for k, v in data_status.items() if not v]
504 # return ", ".join(missing_relations)
507 # def relation_state(self) -> Dict[str, Any]:
508 # """Collects relation state configuration for pod spec assembly.
511 # Dict[str, Any]: relation state information.
514 # "message_host": self.state.message_host,
515 # "message_port": self.state.message_port,
516 # "database_uri": self.state.database_uri,
517 # "ro_host": self.state.ro_host,
518 # "ro_port": self.state.ro_port,
521 # return relation_state
523 # def configure_pod(self, event: EventBase) -> NoReturn:
524 # """Assemble the pod spec and apply it, if possible.
527 # event (EventBase): Hook or Relation event that started the
530 # if missing := self._missing_relations():
531 # self.unit.status = BlockedStatus(
532 # "Waiting for {0} relation{1}".format(
533 # missing, "s" if "," in missing else ""
538 # if not self.unit.is_leader():
539 # self.unit.status = ActiveStatus("ready")
542 # self.unit.status = MaintenanceStatus("Assembling pod spec")
544 # # Fetch image information
546 # self.unit.status = MaintenanceStatus("Fetching image information")
547 # image_info = self.image.fetch()
548 # except OCIImageResourceError:
549 # self.unit.status = BlockedStatus("Error fetching image information")
553 # pod_spec = make_pod_spec(
556 # self.relation_state,
557 # self.model.app.name,
560 # except ValueError as exc:
561 # logger.exception("Config/Relation data validation error")
562 # self.unit.status = BlockedStatus(str(exc))
565 # if self.state.pod_spec != pod_spec:
566 # self.model.pod.set_spec(pod_spec)
567 # self.state.pod_spec = pod_spec
569 # self.unit.status = ActiveStatus("ready")
572 # if __name__ == "__main__":