X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fro%2Fsrc%2Fcharm.py;h=028dc0a4f0c1afd6cd7758fa2c88952fa35cacab;hb=83aed2493a2065cea2e8f25f20728c4292e7ee9e;hp=8e6d5764e40175c4ffb3e617cc39ff4117bc6a70;hpb=ccfacbbb3d3bd025f48f2a1434e0b6bdeae64ead;p=osm%2Fdevops.git diff --git a/installers/charm/ro/src/charm.py b/installers/charm/ro/src/charm.py index 8e6d5764..028dc0a4 100755 --- a/installers/charm/ro/src/charm.py +++ b/installers/charm/ro/src/charm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2020 Canonical Ltd. +# Copyright 2021 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -20,116 +20,156 @@ # osm-charmers@lists.launchpad.net ## +# pylint: disable=E0213 + +import base64 import logging -from typing import Dict, List, NoReturn +from typing import Dict, NoReturn, Optional -from ops.charm import CharmBase -from ops.framework import EventBase, StoredState +from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires from ops.main import main -from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit -from oci_image import OCIImageResource, OCIImageResourceError - -from pod_spec import make_pod_spec +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.interfaces.mongo import MongoClient +from opslib.osm.interfaces.mysql import MysqlClient +from opslib.osm.pod import ( + ContainerV3Builder, + FilesV3Builder, + PodRestartPolicy, + PodSpecV3Builder, +) +from opslib.osm.validator import ModelValidator, validator logger = logging.getLogger(__name__) -RO_PORT = 9090 - - -class RelationsMissing(Exception): - def __init__(self, missing_relations: List): - self.message = "" - if missing_relations and isinstance(missing_relations, list): - self.message += f'Waiting for {", ".join(missing_relations)} relation' - if "," in self.message: - self.message += "s" - +PORT = 9090 + + +def _check_certificate_data(name: str, content: str): + if not name or not content: + raise ValueError("certificate name and content must be a non-empty string") + + +def _extract_certificates(certs_config: str): + certificates = {} + if certs_config: + cert_list = certs_config.split(",") + for cert in cert_list: + name, content = cert.split(":") + _check_certificate_data(name, content) + certificates[name] = content + return certificates + + +def decode(content: str): + return base64.b64decode(content.encode("utf-8")).decode("utf-8") + + +class ConfigModel(ModelValidator): + enable_ng_ro: bool + database_commonkey: str + mongodb_uri: Optional[str] + log_level: str + mysql_host: Optional[str] + mysql_port: Optional[int] + mysql_user: Optional[str] + mysql_password: Optional[str] + mysql_root_password: Optional[str] + vim_database: str + ro_database: str + openmano_tenant: str + certificates: Optional[str] + image_pull_policy: str + debug_mode: bool + security_context: bool + period_refresh_active: Optional[int] + + @validator("log_level") + def validate_log_level(cls, v): + if v not in {"INFO", "DEBUG"}: + raise ValueError("value must be INFO or DEBUG") + return v + + @validator("certificates") + def validate_certificates(cls, v): + # Raises an exception if it cannot extract the certificates + _extract_certificates(v) + return v + + @validator("mongodb_uri") + def validate_mongodb_uri(cls, v): + if v and not v.startswith("mongodb://"): + raise ValueError("mongodb_uri is not properly formed") + return v + + @validator("mysql_port") + def validate_mysql_port(cls, v): + if v and (v <= 0 or v >= 65535): + raise ValueError("Mysql port out of range") + return v + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] -class RelationDefinition: - def __init__(self, relation_name: str, keys: List, source_type): - if source_type != Application and source_type != Unit: - raise TypeError( - "source_type should be ops.model.Application or ops.model.Unit" + @property + def certificates_dict(cls): + return _extract_certificates(cls.certificates) if cls.certificates else {} + + @validator("period_refresh_active") + def validate_vim_refresh_period(cls, v): + if v and v < 60 and v != -1: + raise ValueError( + "Refresh Period is too tight, insert >= 60 seconds or disable using -1" ) - self.relation_name = relation_name - self.keys = keys - self.source_type = source_type - - -def check_missing_relation_data( - data: Dict, - expected_relations_data: List[RelationDefinition], -): - missing_relations = [] - for relation_data in expected_relations_data: - if not all( - f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys - ): - missing_relations.append(relation_data.relation_name) - if missing_relations: - raise RelationsMissing(missing_relations) - - -def get_relation_data( - charm: CharmBase, - relation_data: RelationDefinition, -) -> Dict: - data = {} - relation = charm.model.get_relation(relation_data.relation_name) - if relation: - self_app_unit = ( - charm.app if relation_data.source_type == Application else charm.unit - ) - expected_type = relation_data.source_type - for app_unit in relation.data: - if app_unit != self_app_unit and isinstance(app_unit, expected_type): - if all(k in relation.data[app_unit] for k in relation_data.keys): - for k in relation_data.keys: - data[f"{relation_data.relation_name}_{k}"] = relation.data[ - app_unit - ].get(k) - break - return data + return v -class RoCharm(CharmBase): - """RO Charm.""" +class RoCharm(CharmedOsmBase): + """GrafanaCharm Charm.""" - state = StoredState() + on = KafkaEvents() def __init__(self, *args) -> NoReturn: - """RO Charm constructor.""" - super().__init__(*args) - - # Internal state initialization - self.state.set_default(pod_spec=None) - - self.port = RO_PORT - self.image = OCIImageResource(self, "image") - - # Registering regular events - self.framework.observe(self.on.start, self.configure_pod) - self.framework.observe(self.on.config_changed, self.configure_pod) - - # Registering required relation events - self.framework.observe(self.on.kafka_relation_changed, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod) - self.framework.observe(self.on.mysql_relation_changed, self.configure_pod) + """Prometheus Charm constructor.""" + super().__init__( + *args, + oci_image="image", + vscode_workspace=VSCODE_WORKSPACE, + ) + if self.config.get("debug_mode"): + self.enable_debug_mode( + pubkey=self.config.get("debug_pubkey"), + hostpaths={ + "osm_common": { + "hostpath": self.config.get("debug_common_local_path"), + "container-path": "/usr/lib/python3/dist-packages/osm_common", + }, + **_get_ro_host_paths(self.config.get("debug_ro_local_path")), + }, + ) + self.kafka = KafkaRequires(self) + self.framework.observe(self.on.kafka_available, self.configure_pod) + self.framework.observe(self.on.kafka_broken, self.configure_pod) - # Registering required relation departed events - self.framework.observe(self.on.kafka_relation_departed, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod) - self.framework.observe(self.on.mysql_relation_departed, self.configure_pod) + self.mysql_client = MysqlClient(self, "mysql") + self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod) + self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod) - # Registering required relation broken events - self.framework.observe(self.on.kafka_relation_broken, self.configure_pod) - self.framework.observe(self.on.mongodb_relation_broken, self.configure_pod) - self.framework.observe(self.on.mysql_relation_broken, self.configure_pod) + self.mongodb_client = MongoClient(self, "mongodb") + self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod) + self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod) - # Registering provided relation events - self.framework.observe(self.on.ro_relation_joined, self._publish_ro_info) + self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info) - def _publish_ro_info(self, event: EventBase) -> NoReturn: + def _publish_ro_info(self, event): """Publishes RO information. Args: @@ -138,80 +178,287 @@ class RoCharm(CharmBase): if self.unit.is_leader(): rel_data = { "host": self.model.app.name, - "port": str(RO_PORT), + "port": str(PORT), } for k, v in rel_data.items(): event.relation.data[self.app][k] = v - @property - def relations_requirements(self): - if self.model.config["enable_ng_ro"]: - return [ - RelationDefinition("kafka", ["host", "port"], Unit), - RelationDefinition("mongodb", ["connection_string"], Unit), - ] + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] + + if config.enable_ng_ro: + if not self.kafka.host or not self.kafka.port: + missing_relations.append("kafka") + if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit(): + missing_relations.append("mongodb") else: - return [ - RelationDefinition( - "mysql", ["host", "port", "user", "password", "root_password"], Unit + if not config.mysql_host and self.mysql_client.is_missing_data_in_unit(): + missing_relations.append("mysql") + if missing_relations: + raise RelationsMissing(missing_relations) + + def _validate_mysql_config(self, config: ConfigModel): + invalid_values = [] + if not config.mysql_user: + invalid_values.append("Mysql user is empty") + if not config.mysql_password: + invalid_values.append("Mysql password is empty") + if not config.mysql_root_password: + invalid_values.append("Mysql root password empty") + + if invalid_values: + raise ValueError("Invalid values: " + ", ".join(invalid_values)) + + def _build_cert_files( + self, + config: ConfigModel, + ): + cert_files_builder = FilesV3Builder() + for name, content in config.certificates_dict.items(): + cert_files_builder.add_file(name, decode(content), mode=0o600) + return cert_files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + + if config.enable_ng_ro: + if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit(): + raise Exception( + "Mongodb data cannot be provided via config and relation" ) - ] + else: + if config.mysql_host and not self.mysql_client.is_missing_data_in_unit(): + raise Exception("Mysql data cannot be provided via config and relation") - def get_relation_state(self): - relation_state = {} - for relation_requirements in self.relations_requirements: - data = get_relation_data(self, relation_requirements) - relation_state = {**relation_state, **data} - check_missing_relation_data(relation_state, self.relations_requirements) - return relation_state + if config.mysql_host: + self._validate_mysql_config(config) - def configure_pod(self, _=None) -> NoReturn: - """Assemble the pod spec and apply it, if possible. + # Check relations + self._check_missing_dependencies(config) - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - return - - relation_state = None - try: - relation_state = self.get_relation_state() - except RelationsMissing as exc: - logger.exception("Relation missing error") - self.unit.status = BlockedStatus(exc.message) - return - - self.unit.status = MaintenanceStatus("Assembling pod spec") - - # Fetch image information - try: - self.unit.status = MaintenanceStatus("Fetching image information") - image_info = self.image.fetch() - except OCIImageResourceError: - self.unit.status = BlockedStatus("Error fetching image information") - return - - try: - pod_spec = make_pod_spec( - image_info, - self.model.config, - relation_state, - self.model.app.name, - self.port, - ) - except ValueError as exc: - logger.exception("Config/Relation data validation error") - self.unit.status = BlockedStatus(str(exc)) - return + security_context_enabled = ( + config.security_context if not config.debug_mode else False + ) - if self.state.pod_spec != pod_spec: - self.model.pod.set_spec(pod_spec) - self.state.pod_spec = pod_spec + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder( + enable_security_context=security_context_enabled + ) - self.unit.status = ActiveStatus("ready") + # Build Container + container_builder = ContainerV3Builder( + self.app.name, + image_info, + config.image_pull_policy, + run_as_non_root=security_context_enabled, + ) + certs_files = self._build_cert_files(config) + + if certs_files: + container_builder.add_volume_config("certs", "/certs", certs_files) + + container_builder.add_port(name=self.app.name, port=PORT) + container_builder.add_http_readiness_probe( + "/ro/" if config.enable_ng_ro else "/openmano/tenants", + PORT, + initial_delay_seconds=10, + period_seconds=10, + timeout_seconds=5, + failure_threshold=3, + ) + container_builder.add_http_liveness_probe( + "/ro/" if config.enable_ng_ro else "/openmano/tenants", + PORT, + initial_delay_seconds=600, + period_seconds=10, + timeout_seconds=5, + failure_threshold=3, + ) + container_builder.add_envs( + { + "OSMRO_LOG_LEVEL": config.log_level, + } + ) + if config.period_refresh_active: + container_builder.add_envs( + { + "OSMRO_PERIOD_REFRESH_ACTIVE": config.period_refresh_active, + } + ) + if config.enable_ng_ro: + # Add secrets to the pod + mongodb_secret_name = f"{self.app.name}-mongodb-secret" + pod_spec_builder.add_secret( + mongodb_secret_name, + { + "uri": config.mongodb_uri or self.mongodb_client.connection_string, + "commonkey": config.database_commonkey, + }, + ) + container_builder.add_envs( + { + "OSMRO_MESSAGE_DRIVER": "kafka", + "OSMRO_MESSAGE_HOST": self.kafka.host, + "OSMRO_MESSAGE_PORT": self.kafka.port, + # MongoDB configuration + "OSMRO_DATABASE_DRIVER": "mongo", + } + ) + container_builder.add_secret_envs( + secret_name=mongodb_secret_name, + envs={ + "OSMRO_DATABASE_URI": "uri", + "OSMRO_DATABASE_COMMONKEY": "commonkey", + }, + ) + restart_policy = PodRestartPolicy() + restart_policy.add_secrets(secret_names=(mongodb_secret_name,)) + pod_spec_builder.set_restart_policy(restart_policy) + + else: + container_builder.add_envs( + { + "RO_DB_HOST": config.mysql_host or self.mysql_client.host, + "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host, + "RO_DB_PORT": config.mysql_port or self.mysql_client.port, + "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port, + "RO_DB_USER": config.mysql_user or self.mysql_client.user, + "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user, + "RO_DB_PASSWORD": config.mysql_password + or self.mysql_client.password, + "RO_DB_OVIM_PASSWORD": config.mysql_password + or self.mysql_client.password, + "RO_DB_ROOT_PASSWORD": config.mysql_root_password + or self.mysql_client.root_password, + "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password + or self.mysql_client.root_password, + "RO_DB_NAME": config.ro_database, + "RO_DB_OVIM_NAME": config.vim_database, + "OPENMANO_TENANT": config.openmano_tenant, + } + ) + container = container_builder.build() + + # Add container to pod spec + pod_spec_builder.add_container(container) + + return pod_spec_builder.build() + + +VSCODE_WORKSPACE = { + "folders": [ + {"path": "/usr/lib/python3/dist-packages/osm_ng_ro"}, + {"path": "/usr/lib/python3/dist-packages/osm_common"}, + {"path": "/usr/lib/python3/dist-packages/osm_ro_plugin"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls"}, + {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_aws"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_azure"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_gcp"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_openstack"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_openvim"}, + {"path": "/usr/lib/python3/dist-packages/osm_rovim_vmware"}, + ], + "launch": { + "configurations": [ + { + "module": "osm_ng_ro.ro_main", + "name": "NG RO", + "request": "launch", + "type": "python", + "justMyCode": False, + } + ], + "version": "0.2.0", + }, + "settings": {}, +} + + +def _get_ro_host_paths(ro_host_path: str) -> Dict: + """Get RO host paths""" + return ( + { + "NG-RO": { + "hostpath": f"{ro_host_path}/NG-RO", + "container-path": "/usr/lib/python3/dist-packages/osm_ng_ro", + }, + "RO-plugin": { + "hostpath": f"{ro_host_path}/RO-plugin", + "container-path": "/usr/lib/python3/dist-packages/osm_ro_plugin", + }, + "RO-SDN-arista_cloudvision": { + "hostpath": f"{ro_host_path}/RO-SDN-arista_cloudvision", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision", + }, + "RO-SDN-dpb": { + "hostpath": f"{ro_host_path}/RO-SDN-dpb", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb", + }, + "RO-SDN-dynpac": { + "hostpath": f"{ro_host_path}/RO-SDN-dynpac", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac", + }, + "RO-SDN-floodlight_openflow": { + "hostpath": f"{ro_host_path}/RO-SDN-floodlight_openflow", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof", + }, + "RO-SDN-ietfl2vpn": { + "hostpath": f"{ro_host_path}/RO-SDN-ietfl2vpn", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn", + }, + "RO-SDN-juniper_contrail": { + "hostpath": f"{ro_host_path}/RO-SDN-juniper_contrail", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail", + }, + "RO-SDN-odl_openflow": { + "hostpath": f"{ro_host_path}/RO-SDN-odl_openflow", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof", + }, + "RO-SDN-onos_openflow": { + "hostpath": f"{ro_host_path}/RO-SDN-onos_openflow", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof", + }, + "RO-SDN-onos_vpls": { + "hostpath": f"{ro_host_path}/RO-SDN-onos_vpls", + "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls", + }, + "RO-VIM-aws": { + "hostpath": f"{ro_host_path}/RO-VIM-aws", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_aws", + }, + "RO-VIM-azure": { + "hostpath": f"{ro_host_path}/RO-VIM-azure", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_azure", + }, + "RO-VIM-gcp": { + "hostpath": f"{ro_host_path}/RO-VIM-gcp", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_gcp", + }, + "RO-VIM-openstack": { + "hostpath": f"{ro_host_path}/RO-VIM-openstack", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_openstack", + }, + "RO-VIM-openvim": { + "hostpath": f"{ro_host_path}/RO-VIM-openvim", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_openvim", + }, + "RO-VIM-vmware": { + "hostpath": f"{ro_host_path}/RO-VIM-vmware", + "container-path": "/usr/lib/python3/dist-packages/osm_rovim_vmware", + }, + } + if ro_host_path + else {} + ) if __name__ == "__main__":