Make tcpsocket readiness and liveness configurable
[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 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
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 on = KafkaEvents()
144
145 def __init__(self, *args) -> NoReturn:
146 super().__init__(
147 *args,
148 oci_image="image",
149 vscode_workspace=VSCODE_WORKSPACE,
150 )
151 if self.config.get("debug_mode"):
152 self.enable_debug_mode(
153 pubkey=self.config.get("debug_pubkey"),
154 hostpaths={
155 "LCM": {
156 "hostpath": self.config.get("debug_lcm_local_path"),
157 "container-path": "/usr/lib/python3/dist-packages/osm_lcm",
158 },
159 "N2VC": {
160 "hostpath": self.config.get("debug_n2vc_local_path"),
161 "container-path": "/usr/lib/python3/dist-packages/n2vc",
162 },
163 "osm_common": {
164 "hostpath": self.config.get("debug_common_local_path"),
165 "container-path": "/usr/lib/python3/dist-packages/osm_common",
166 },
167 },
168 )
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)
172
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)
176
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)
180
181 def _check_missing_dependencies(self, config: ConfigModel):
182 missing_relations = []
183
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")
190
191 if missing_relations:
192 raise RelationsMissing(missing_relations)
193
194 def build_pod_spec(self, image_info):
195 # Validate config
196 config = ConfigModel(**dict(self.config))
197
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")
200
201 # Check relations
202 self._check_missing_dependencies(config)
203
204 security_context_enabled = (
205 config.security_context if not config.debug_mode else False
206 )
207
208 # Create Builder for the PodSpec
209 pod_spec_builder = PodSpecV3Builder(
210 enable_security_context=security_context_enabled
211 )
212
213 # Add secrets to the pod
214 mongodb_secret_name = f"{self.app.name}-mongodb-secret"
215 pod_spec_builder.add_secret(
216 mongodb_secret_name,
217 {
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,
221 },
222 )
223
224 # Build Container
225 container_builder = ContainerV3Builder(
226 self.app.name,
227 image_info,
228 config.image_pull_policy,
229 run_as_non_root=security_context_enabled,
230 )
231 container_builder.add_port(name=self.app.name, port=PORT)
232 container_builder.add_envs(
233 {
234 # General configuration
235 "ALLOW_ANONYMOUS_LOGIN": "yes",
236 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
237 # RO configuration
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,
252 }
253 )
254 container_builder.add_secret_envs(
255 secret_name=mongodb_secret_name,
256 envs={
257 "OSMLCM_DATABASE_URI": "uri",
258 "OSMLCM_DATABASE_COMMONKEY": "commonkey",
259 "OSMLCM_STORAGE_URI": "uri",
260 "OSMLCM_VCA_HELM_CA_CERTS": "helm_ca_certs",
261 },
262 )
263 if config.vca_host:
264 vca_secret_name = f"{self.app.name}-vca-secret"
265 pod_spec_builder.add_secret(
266 vca_secret_name,
267 {
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,
276 },
277 )
278 container_builder.add_secret_envs(
279 secret_name=vca_secret_name,
280 envs={
281 # VCA configuration
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",
290 },
291 )
292 if config.vca_apiproxy:
293 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
294
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")
299 }
300 if model_config_envs:
301 container_builder.add_envs(model_config_envs)
302 container = container_builder.build()
303
304 # Add container to pod spec
305 pod_spec_builder.add_container(container)
306
307 # Add restart policy
308 restart_policy = PodRestartPolicy()
309 restart_policy.add_secrets()
310 pod_spec_builder.set_restart_policy(restart_policy)
311
312 return pod_spec_builder.build()
313
314
315 VSCODE_WORKSPACE = {
316 "folders": [
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"},
320 ],
321 "settings": {},
322 "launch": {
323 "version": "0.2.0",
324 "configurations": [
325 {
326 "name": "LCM",
327 "type": "python",
328 "request": "launch",
329 "module": "osm_lcm.lcm",
330 "justMyCode": False,
331 }
332 ],
333 },
334 }
335
336
337 if __name__ == "__main__":
338 main(LcmCharm)
339
340
341 # class ConfigurePodEvent(EventBase):
342 # """Configure Pod event"""
343
344 # pass
345
346
347 # class LcmEvents(CharmEvents):
348 # """LCM Events"""
349
350 # configure_pod = EventSource(ConfigurePodEvent)
351
352
353 # class LcmCharm(CharmBase):
354 # """LCM Charm."""
355
356 # state = StoredState()
357 # on = LcmEvents()
358
359 # def __init__(self, *args) -> NoReturn:
360 # """LCM Charm constructor."""
361 # super().__init__(*args)
362
363 # # Internal state initialization
364 # self.state.set_default(pod_spec=None)
365
366 # # Message bus data initialization
367 # self.state.set_default(message_host=None)
368 # self.state.set_default(message_port=None)
369
370 # # Database data initialization
371 # self.state.set_default(database_uri=None)
372
373 # # RO data initialization
374 # self.state.set_default(ro_host=None)
375 # self.state.set_default(ro_port=None)
376
377 # self.port = LCM_PORT
378 # self.image = OCIImageResource(self, "image")
379
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)
384
385 # # Registering custom internal events
386 # self.framework.observe(self.on.configure_pod, self.configure_pod)
387
388 # # Registering required relation events
389 # self.framework.observe(
390 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
391 # )
392 # self.framework.observe(
393 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
394 # )
395 # self.framework.observe(
396 # self.on.ro_relation_changed, self._on_ro_relation_changed
397 # )
398
399 # # Registering required relation broken events
400 # self.framework.observe(
401 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
402 # )
403 # self.framework.observe(
404 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
405 # )
406 # self.framework.observe(
407 # self.on.ro_relation_broken, self._on_ro_relation_broken
408 # )
409
410 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
411 # """Reads information about the kafka relation.
412
413 # Args:
414 # event (EventBase): Kafka relation event.
415 # """
416 # message_host = event.relation.data[event.unit].get("host")
417 # message_port = event.relation.data[event.unit].get("port")
418
419 # if (
420 # message_host
421 # and message_port
422 # and (
423 # self.state.message_host != message_host
424 # or self.state.message_port != message_port
425 # )
426 # ):
427 # self.state.message_host = message_host
428 # self.state.message_port = message_port
429 # self.on.configure_pod.emit()
430
431 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
432 # """Clears data from kafka relation.
433
434 # Args:
435 # event (EventBase): Kafka relation event.
436 # """
437 # self.state.message_host = None
438 # self.state.message_port = None
439 # self.on.configure_pod.emit()
440
441 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
442 # """Reads information about the DB relation.
443
444 # Args:
445 # event (EventBase): DB relation event.
446 # """
447 # database_uri = event.relation.data[event.unit].get("connection_string")
448
449 # if database_uri and self.state.database_uri != database_uri:
450 # self.state.database_uri = database_uri
451 # self.on.configure_pod.emit()
452
453 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
454 # """Clears data from mongodb relation.
455
456 # Args:
457 # event (EventBase): DB relation event.
458 # """
459 # self.state.database_uri = None
460 # self.on.configure_pod.emit()
461
462 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
463 # """Reads information about the RO relation.
464
465 # Args:
466 # event (EventBase): Keystone relation event.
467 # """
468 # ro_host = event.relation.data[event.unit].get("host")
469 # ro_port = event.relation.data[event.unit].get("port")
470
471 # if (
472 # ro_host
473 # and ro_port
474 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
475 # ):
476 # self.state.ro_host = ro_host
477 # self.state.ro_port = ro_port
478 # self.on.configure_pod.emit()
479
480 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
481 # """Clears data from ro relation.
482
483 # Args:
484 # event (EventBase): Keystone relation event.
485 # """
486 # self.state.ro_host = None
487 # self.state.ro_port = None
488 # self.on.configure_pod.emit()
489
490 # def _missing_relations(self) -> str:
491 # """Checks if there missing relations.
492
493 # Returns:
494 # str: string with missing relations
495 # """
496 # data_status = {
497 # "kafka": self.state.message_host,
498 # "mongodb": self.state.database_uri,
499 # "ro": self.state.ro_host,
500 # }
501
502 # missing_relations = [k for k, v in data_status.items() if not v]
503
504 # return ", ".join(missing_relations)
505
506 # @property
507 # def relation_state(self) -> Dict[str, Any]:
508 # """Collects relation state configuration for pod spec assembly.
509
510 # Returns:
511 # Dict[str, Any]: relation state information.
512 # """
513 # relation_state = {
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,
519 # }
520
521 # return relation_state
522
523 # def configure_pod(self, event: EventBase) -> NoReturn:
524 # """Assemble the pod spec and apply it, if possible.
525
526 # Args:
527 # event (EventBase): Hook or Relation event that started the
528 # function.
529 # """
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 ""
534 # )
535 # )
536 # return
537
538 # if not self.unit.is_leader():
539 # self.unit.status = ActiveStatus("ready")
540 # return
541
542 # self.unit.status = MaintenanceStatus("Assembling pod spec")
543
544 # # Fetch image information
545 # try:
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")
550 # return
551
552 # try:
553 # pod_spec = make_pod_spec(
554 # image_info,
555 # self.model.config,
556 # self.relation_state,
557 # self.model.app.name,
558 # self.port,
559 # )
560 # except ValueError as exc:
561 # logger.exception("Config/Relation data validation error")
562 # self.unit.status = BlockedStatus(str(exc))
563 # return
564
565 # if self.state.pod_spec != pod_spec:
566 # self.model.pod.set_spec(pod_spec)
567 # self.state.pod_spec = pod_spec
568
569 # self.unit.status = ActiveStatus("ready")
570
571
572 # if __name__ == "__main__":
573 # main(LcmCharm)