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 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
39 logger
= logging
.getLogger(__name__
)
44 class ConfigModel(ModelValidator
):
53 database_commonkey
: str
55 vca_apiproxy
: Optional
[str]
56 # Model-config options
57 vca_model_config_agent_metadata_url
: Optional
[str]
58 vca_model_config_agent_stream
: Optional
[str]
59 vca_model_config_apt_ftp_proxy
: Optional
[str]
60 vca_model_config_apt_http_proxy
: Optional
[str]
61 vca_model_config_apt_https_proxy
: Optional
[str]
62 vca_model_config_apt_mirror
: Optional
[str]
63 vca_model_config_apt_no_proxy
: Optional
[str]
64 vca_model_config_automatically_retry_hooks
: Optional
[bool]
65 vca_model_config_backup_dir
: Optional
[str]
66 vca_model_config_cloudinit_userdata
: Optional
[str]
67 vca_model_config_container_image_metadata_url
: Optional
[str]
68 vca_model_config_container_image_stream
: Optional
[str]
69 vca_model_config_container_inherit_properties
: Optional
[str]
70 vca_model_config_container_networking_method
: Optional
[str]
71 vca_model_config_default_series
: Optional
[str]
72 vca_model_config_default_space
: Optional
[str]
73 vca_model_config_development
: Optional
[bool]
74 vca_model_config_disable_network_management
: Optional
[bool]
75 vca_model_config_egress_subnets
: Optional
[str]
76 vca_model_config_enable_os_refresh_update
: Optional
[bool]
77 vca_model_config_enable_os_upgrade
: Optional
[bool]
78 vca_model_config_fan_config
: Optional
[str]
79 vca_model_config_firewall_mode
: Optional
[str]
80 vca_model_config_ftp_proxy
: Optional
[str]
81 vca_model_config_http_proxy
: Optional
[str]
82 vca_model_config_https_proxy
: Optional
[str]
83 vca_model_config_ignore_machine_addresses
: Optional
[bool]
84 vca_model_config_image_metadata_url
: Optional
[str]
85 vca_model_config_image_stream
: Optional
[str]
86 vca_model_config_juju_ftp_proxy
: Optional
[str]
87 vca_model_config_juju_http_proxy
: Optional
[str]
88 vca_model_config_juju_https_proxy
: Optional
[str]
89 vca_model_config_juju_no_proxy
: Optional
[str]
90 vca_model_config_logforward_enabled
: Optional
[bool]
91 vca_model_config_logging_config
: Optional
[str]
92 vca_model_config_lxd_snap_channel
: Optional
[str]
93 vca_model_config_max_action_results_age
: Optional
[str]
94 vca_model_config_max_action_results_size
: Optional
[str]
95 vca_model_config_max_status_history_age
: Optional
[str]
96 vca_model_config_max_status_history_size
: Optional
[str]
97 vca_model_config_net_bond_reconfigure_delay
: Optional
[str]
98 vca_model_config_no_proxy
: Optional
[str]
99 vca_model_config_provisioner_harvest_mode
: Optional
[str]
100 vca_model_config_proxy_ssh
: Optional
[bool]
101 vca_model_config_snap_http_proxy
: Optional
[str]
102 vca_model_config_snap_https_proxy
: Optional
[str]
103 vca_model_config_snap_store_assertions
: Optional
[str]
104 vca_model_config_snap_store_proxy
: Optional
[str]
105 vca_model_config_snap_store_proxy_url
: Optional
[str]
106 vca_model_config_ssl_hostname_verification
: Optional
[bool]
107 vca_model_config_test_mode
: Optional
[bool]
108 vca_model_config_transmit_vendor_metrics
: Optional
[bool]
109 vca_model_config_update_status_hook_interval
: Optional
[str]
111 @validator("log_level")
112 def validate_log_level(cls
, v
):
113 if v
not in {"INFO", "DEBUG"}:
114 raise ValueError("value must be INFO or DEBUG")
118 class LcmCharm(CharmedOsmBase
):
119 def __init__(self
, *args
) -> NoReturn
:
120 super().__init
__(*args
, oci_image
="image")
122 self
.kafka_client
= KafkaClient(self
, "kafka")
123 self
.framework
.observe(self
.on
["kafka"].relation_changed
, self
.configure_pod
)
124 self
.framework
.observe(self
.on
["kafka"].relation_broken
, self
.configure_pod
)
126 self
.mongodb_client
= MongoClient(self
, "mongodb")
127 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
128 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
130 self
.ro_client
= HttpClient(self
, "ro")
131 self
.framework
.observe(self
.on
["ro"].relation_changed
, self
.configure_pod
)
132 self
.framework
.observe(self
.on
["ro"].relation_broken
, self
.configure_pod
)
134 def _check_missing_dependencies(self
, config
: ConfigModel
):
135 missing_relations
= []
137 if self
.kafka_client
.is_missing_data_in_unit():
138 missing_relations
.append("kafka")
139 if self
.mongodb_client
.is_missing_data_in_unit():
140 missing_relations
.append("mongodb")
141 if self
.ro_client
.is_missing_data_in_app():
142 missing_relations
.append("ro")
144 if missing_relations
:
145 raise RelationsMissing(missing_relations
)
147 def build_pod_spec(self
, image_info
):
149 config
= ConfigModel(**dict(self
.config
))
151 self
._check
_missing
_dependencies
(config
)
152 # Create Builder for the PodSpec
153 pod_spec_builder
= PodSpecV3Builder()
155 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
156 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
157 container_builder
.add_envs(
159 # General configuration
160 "ALLOW_ANONYMOUS_LOGIN": "yes",
161 "OSMLCM_GLOBAL_LOGLEVEL": config
.log_level
,
163 "OSMLCM_RO_HOST": self
.ro_client
.host
,
164 "OSMLCM_RO_PORT": self
.ro_client
.port
,
165 "OSMLCM_RO_TENANT": "osm",
166 # Kafka configuration
167 "OSMLCM_MESSAGE_DRIVER": "kafka",
168 "OSMLCM_MESSAGE_HOST": self
.kafka_client
.host
,
169 "OSMLCM_MESSAGE_PORT": self
.kafka_client
.port
,
170 # Database configuration
171 "OSMLCM_DATABASE_DRIVER": "mongo",
172 "OSMLCM_DATABASE_URI": self
.mongodb_client
.connection_string
,
173 "OSMLCM_DATABASE_COMMONKEY": config
.database_commonkey
,
174 # Storage configuration
175 "OSMLCM_STORAGE_DRIVER": "mongo",
176 "OSMLCM_STORAGE_PATH": "/app/storage",
177 "OSMLCM_STORAGE_COLLECTION": "files",
178 "OSMLCM_STORAGE_URI": self
.mongodb_client
.connection_string
,
180 "OSMLCM_VCA_HOST": config
.vca_host
,
181 "OSMLCM_VCA_PORT": config
.vca_port
,
182 "OSMLCM_VCA_USER": config
.vca_user
,
183 "OSMLCM_VCA_PUBKEY": config
.vca_pubkey
,
184 "OSMLCM_VCA_SECRET": config
.vca_secret
,
185 "OSMLCM_VCA_CACERT": config
.vca_cacert
,
186 "OSMLCM_VCA_CLOUD": config
.vca_cloud
,
187 "OSMLCM_VCA_K8S_CLOUD": config
.vca_k8s_cloud
,
190 if config
.vca_apiproxy
:
191 container_builder
.add_env("OSMLCM_VCA_APIPROXY", config
.vca_apiproxy
)
193 model_config_envs
= {
194 f
"OSMLCM_{k.upper()}": v
195 for k
, v
in self
.config
.items()
196 if k
.startswith("vca_model_config")
198 if model_config_envs
:
199 container_builder
.add_envs(model_config_envs
)
200 container
= container_builder
.build()
201 # Add container to pod spec
202 pod_spec_builder
.add_container(container
)
203 return pod_spec_builder
.build()
206 if __name__
== "__main__":
210 # class ConfigurePodEvent(EventBase):
211 # """Configure Pod event"""
216 # class LcmEvents(CharmEvents):
219 # configure_pod = EventSource(ConfigurePodEvent)
222 # class LcmCharm(CharmBase):
225 # state = StoredState()
228 # def __init__(self, *args) -> NoReturn:
229 # """LCM Charm constructor."""
230 # super().__init__(*args)
232 # # Internal state initialization
233 # self.state.set_default(pod_spec=None)
235 # # Message bus data initialization
236 # self.state.set_default(message_host=None)
237 # self.state.set_default(message_port=None)
239 # # Database data initialization
240 # self.state.set_default(database_uri=None)
242 # # RO data initialization
243 # self.state.set_default(ro_host=None)
244 # self.state.set_default(ro_port=None)
246 # self.port = LCM_PORT
247 # self.image = OCIImageResource(self, "image")
249 # # Registering regular events
250 # self.framework.observe(self.on.start, self.configure_pod)
251 # self.framework.observe(self.on.config_changed, self.configure_pod)
252 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
254 # # Registering custom internal events
255 # self.framework.observe(self.on.configure_pod, self.configure_pod)
257 # # Registering required relation events
258 # self.framework.observe(
259 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
261 # self.framework.observe(
262 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
264 # self.framework.observe(
265 # self.on.ro_relation_changed, self._on_ro_relation_changed
268 # # Registering required relation broken events
269 # self.framework.observe(
270 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
272 # self.framework.observe(
273 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
275 # self.framework.observe(
276 # self.on.ro_relation_broken, self._on_ro_relation_broken
279 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
280 # """Reads information about the kafka relation.
283 # event (EventBase): Kafka relation event.
285 # message_host = event.relation.data[event.unit].get("host")
286 # message_port = event.relation.data[event.unit].get("port")
292 # self.state.message_host != message_host
293 # or self.state.message_port != message_port
296 # self.state.message_host = message_host
297 # self.state.message_port = message_port
298 # self.on.configure_pod.emit()
300 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
301 # """Clears data from kafka relation.
304 # event (EventBase): Kafka relation event.
306 # self.state.message_host = None
307 # self.state.message_port = None
308 # self.on.configure_pod.emit()
310 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
311 # """Reads information about the DB relation.
314 # event (EventBase): DB relation event.
316 # database_uri = event.relation.data[event.unit].get("connection_string")
318 # if database_uri and self.state.database_uri != database_uri:
319 # self.state.database_uri = database_uri
320 # self.on.configure_pod.emit()
322 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
323 # """Clears data from mongodb relation.
326 # event (EventBase): DB relation event.
328 # self.state.database_uri = None
329 # self.on.configure_pod.emit()
331 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
332 # """Reads information about the RO relation.
335 # event (EventBase): Keystone relation event.
337 # ro_host = event.relation.data[event.unit].get("host")
338 # ro_port = event.relation.data[event.unit].get("port")
343 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
345 # self.state.ro_host = ro_host
346 # self.state.ro_port = ro_port
347 # self.on.configure_pod.emit()
349 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
350 # """Clears data from ro relation.
353 # event (EventBase): Keystone relation event.
355 # self.state.ro_host = None
356 # self.state.ro_port = None
357 # self.on.configure_pod.emit()
359 # def _missing_relations(self) -> str:
360 # """Checks if there missing relations.
363 # str: string with missing relations
366 # "kafka": self.state.message_host,
367 # "mongodb": self.state.database_uri,
368 # "ro": self.state.ro_host,
371 # missing_relations = [k for k, v in data_status.items() if not v]
373 # return ", ".join(missing_relations)
376 # def relation_state(self) -> Dict[str, Any]:
377 # """Collects relation state configuration for pod spec assembly.
380 # Dict[str, Any]: relation state information.
383 # "message_host": self.state.message_host,
384 # "message_port": self.state.message_port,
385 # "database_uri": self.state.database_uri,
386 # "ro_host": self.state.ro_host,
387 # "ro_port": self.state.ro_port,
390 # return relation_state
392 # def configure_pod(self, event: EventBase) -> NoReturn:
393 # """Assemble the pod spec and apply it, if possible.
396 # event (EventBase): Hook or Relation event that started the
399 # if missing := self._missing_relations():
400 # self.unit.status = BlockedStatus(
401 # "Waiting for {0} relation{1}".format(
402 # missing, "s" if "," in missing else ""
407 # if not self.unit.is_leader():
408 # self.unit.status = ActiveStatus("ready")
411 # self.unit.status = MaintenanceStatus("Assembling pod spec")
413 # # Fetch image information
415 # self.unit.status = MaintenanceStatus("Fetching image information")
416 # image_info = self.image.fetch()
417 # except OCIImageResourceError:
418 # self.unit.status = BlockedStatus("Error fetching image information")
422 # pod_spec = make_pod_spec(
425 # self.relation_state,
426 # self.model.app.name,
429 # except ValueError as exc:
430 # logger.exception("Config/Relation data validation error")
431 # self.unit.status = BlockedStatus(str(exc))
434 # if self.state.pod_spec != pod_spec:
435 # self.model.pod.set_spec(pod_spec)
436 # self.state.pod_spec = pod_spec
438 # self.unit.status = ActiveStatus("ready")
441 # if __name__ == "__main__":