Fix 1473 and add vca_model_config_* to lcm charm
[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: str
46 vca_port: int
47 vca_user: str
48 vca_secret: str
49 vca_pubkey: str
50 vca_cacert: str
51 vca_cloud: str
52 vca_k8s_cloud: str
53 database_commonkey: str
54 log_level: 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]
110
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")
115 return v
116
117
118 class LcmCharm(CharmedOsmBase):
119 def __init__(self, *args) -> NoReturn:
120 super().__init__(*args, oci_image="image")
121
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)
125
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)
129
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)
133
134 def _check_missing_dependencies(self, config: ConfigModel):
135 missing_relations = []
136
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")
143
144 if missing_relations:
145 raise RelationsMissing(missing_relations)
146
147 def build_pod_spec(self, image_info):
148 # Validate config
149 config = ConfigModel(**dict(self.config))
150 # Check relations
151 self._check_missing_dependencies(config)
152 # Create Builder for the PodSpec
153 pod_spec_builder = PodSpecV3Builder()
154 # Build Container
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(
158 {
159 # General configuration
160 "ALLOW_ANONYMOUS_LOGIN": "yes",
161 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
162 # RO configuration
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,
179 # VCA configuration
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,
188 }
189 )
190 if config.vca_apiproxy:
191 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
192
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")
197 }
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()
204
205
206 if __name__ == "__main__":
207 main(LcmCharm)
208
209
210 # class ConfigurePodEvent(EventBase):
211 # """Configure Pod event"""
212
213 # pass
214
215
216 # class LcmEvents(CharmEvents):
217 # """LCM Events"""
218
219 # configure_pod = EventSource(ConfigurePodEvent)
220
221
222 # class LcmCharm(CharmBase):
223 # """LCM Charm."""
224
225 # state = StoredState()
226 # on = LcmEvents()
227
228 # def __init__(self, *args) -> NoReturn:
229 # """LCM Charm constructor."""
230 # super().__init__(*args)
231
232 # # Internal state initialization
233 # self.state.set_default(pod_spec=None)
234
235 # # Message bus data initialization
236 # self.state.set_default(message_host=None)
237 # self.state.set_default(message_port=None)
238
239 # # Database data initialization
240 # self.state.set_default(database_uri=None)
241
242 # # RO data initialization
243 # self.state.set_default(ro_host=None)
244 # self.state.set_default(ro_port=None)
245
246 # self.port = LCM_PORT
247 # self.image = OCIImageResource(self, "image")
248
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)
253
254 # # Registering custom internal events
255 # self.framework.observe(self.on.configure_pod, self.configure_pod)
256
257 # # Registering required relation events
258 # self.framework.observe(
259 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
260 # )
261 # self.framework.observe(
262 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
263 # )
264 # self.framework.observe(
265 # self.on.ro_relation_changed, self._on_ro_relation_changed
266 # )
267
268 # # Registering required relation broken events
269 # self.framework.observe(
270 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
271 # )
272 # self.framework.observe(
273 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
274 # )
275 # self.framework.observe(
276 # self.on.ro_relation_broken, self._on_ro_relation_broken
277 # )
278
279 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
280 # """Reads information about the kafka relation.
281
282 # Args:
283 # event (EventBase): Kafka relation event.
284 # """
285 # message_host = event.relation.data[event.unit].get("host")
286 # message_port = event.relation.data[event.unit].get("port")
287
288 # if (
289 # message_host
290 # and message_port
291 # and (
292 # self.state.message_host != message_host
293 # or self.state.message_port != message_port
294 # )
295 # ):
296 # self.state.message_host = message_host
297 # self.state.message_port = message_port
298 # self.on.configure_pod.emit()
299
300 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
301 # """Clears data from kafka relation.
302
303 # Args:
304 # event (EventBase): Kafka relation event.
305 # """
306 # self.state.message_host = None
307 # self.state.message_port = None
308 # self.on.configure_pod.emit()
309
310 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
311 # """Reads information about the DB relation.
312
313 # Args:
314 # event (EventBase): DB relation event.
315 # """
316 # database_uri = event.relation.data[event.unit].get("connection_string")
317
318 # if database_uri and self.state.database_uri != database_uri:
319 # self.state.database_uri = database_uri
320 # self.on.configure_pod.emit()
321
322 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
323 # """Clears data from mongodb relation.
324
325 # Args:
326 # event (EventBase): DB relation event.
327 # """
328 # self.state.database_uri = None
329 # self.on.configure_pod.emit()
330
331 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
332 # """Reads information about the RO relation.
333
334 # Args:
335 # event (EventBase): Keystone relation event.
336 # """
337 # ro_host = event.relation.data[event.unit].get("host")
338 # ro_port = event.relation.data[event.unit].get("port")
339
340 # if (
341 # ro_host
342 # and ro_port
343 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
344 # ):
345 # self.state.ro_host = ro_host
346 # self.state.ro_port = ro_port
347 # self.on.configure_pod.emit()
348
349 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
350 # """Clears data from ro relation.
351
352 # Args:
353 # event (EventBase): Keystone relation event.
354 # """
355 # self.state.ro_host = None
356 # self.state.ro_port = None
357 # self.on.configure_pod.emit()
358
359 # def _missing_relations(self) -> str:
360 # """Checks if there missing relations.
361
362 # Returns:
363 # str: string with missing relations
364 # """
365 # data_status = {
366 # "kafka": self.state.message_host,
367 # "mongodb": self.state.database_uri,
368 # "ro": self.state.ro_host,
369 # }
370
371 # missing_relations = [k for k, v in data_status.items() if not v]
372
373 # return ", ".join(missing_relations)
374
375 # @property
376 # def relation_state(self) -> Dict[str, Any]:
377 # """Collects relation state configuration for pod spec assembly.
378
379 # Returns:
380 # Dict[str, Any]: relation state information.
381 # """
382 # relation_state = {
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,
388 # }
389
390 # return relation_state
391
392 # def configure_pod(self, event: EventBase) -> NoReturn:
393 # """Assemble the pod spec and apply it, if possible.
394
395 # Args:
396 # event (EventBase): Hook or Relation event that started the
397 # function.
398 # """
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 ""
403 # )
404 # )
405 # return
406
407 # if not self.unit.is_leader():
408 # self.unit.status = ActiveStatus("ready")
409 # return
410
411 # self.unit.status = MaintenanceStatus("Assembling pod spec")
412
413 # # Fetch image information
414 # try:
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")
419 # return
420
421 # try:
422 # pod_spec = make_pod_spec(
423 # image_info,
424 # self.model.config,
425 # self.relation_state,
426 # self.model.app.name,
427 # self.port,
428 # )
429 # except ValueError as exc:
430 # logger.exception("Config/Relation data validation error")
431 # self.unit.status = BlockedStatus(str(exc))
432 # return
433
434 # if self.state.pod_spec != pod_spec:
435 # self.model.pod.set_spec(pod_spec)
436 # self.state.pod_spec = pod_spec
437
438 # self.unit.status = ActiveStatus("ready")
439
440
441 # if __name__ == "__main__":
442 # main(LcmCharm)