| #!/usr/bin/env python3 |
| # 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 |
| # a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| # |
| # For those usages not covered by the Apache License, Version 2.0 please |
| # contact: legal@canonical.com |
| # |
| # To get in touch with the maintainers, please contact: |
| # osm-charmers@lists.launchpad.net |
| ## |
| |
| # pylint: disable=E0213 |
| |
| import base64 |
| import logging |
| from typing import Dict, NoReturn, Optional |
| |
| from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires |
| from ops.main import main |
| 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__) |
| |
| 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 |
| |
| @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] |
| |
| @property |
| def certificates_dict(cls): |
| return _extract_certificates(cls.certificates) if cls.certificates else {} |
| |
| |
| class RoCharm(CharmedOsmBase): |
| """GrafanaCharm Charm.""" |
| |
| on = KafkaEvents() |
| |
| def __init__(self, *args) -> NoReturn: |
| """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) |
| |
| 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) |
| |
| 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) |
| |
| self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info) |
| |
| def _publish_ro_info(self, event): |
| """Publishes RO information. |
| |
| Args: |
| event (EventBase): RO relation event. |
| """ |
| if self.unit.is_leader(): |
| rel_data = { |
| "host": self.model.app.name, |
| "port": str(PORT), |
| } |
| for k, v in rel_data.items(): |
| event.relation.data[self.app][k] = v |
| |
| 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: |
| 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") |
| |
| if config.mysql_host: |
| self._validate_mysql_config(config) |
| |
| # Check relations |
| self._check_missing_dependencies(config) |
| |
| security_context_enabled = ( |
| config.security_context if not config.debug_mode else False |
| ) |
| |
| # Create Builder for the PodSpec |
| pod_spec_builder = PodSpecV3Builder( |
| enable_security_context=security_context_enabled |
| ) |
| |
| # 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.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_fos"}, |
| # {"path": "/usr/lib/python3/dist-packages/osm_rovim_opennebula"}, |
| {"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-fos": { |
| "hostpath": f"{ro_host_path}/RO-VIM-fos", |
| "container-path": "/usr/lib/python3/dist-packages/osm_rovim_fos", |
| }, |
| "RO-VIM-opennebula": { |
| "hostpath": f"{ro_host_path}/RO-VIM-opennebula", |
| "container-path": "/usr/lib/python3/dist-packages/osm_rovim_opennebula", |
| }, |
| "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__": |
| main(RoCharm) |