X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2Fdevops.git;a=blobdiff_plain;f=installers%2Fcharm%2Fkeystone%2Fsrc%2Fcharm.py;h=72d70906328c95d19240b5436cb29eeca070b527;hp=d5c169b680ac829457d4c7d0c127827f42ced5a0;hb=49379ced23b5e344a773ce77ac9cb59c1864e19b;hpb=e5fd208248a496cd798a66cb27216e83f2852a76 diff --git a/installers/charm/keystone/src/charm.py b/installers/charm/keystone/src/charm.py index d5c169b6..72d70906 100755 --- a/installers/charm/keystone/src/charm.py +++ b/installers/charm/keystone/src/charm.py @@ -1,44 +1,59 @@ #!/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 a copy of the License at +# 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 +# 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. +# 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 json import logging +from cryptography.fernet import Fernet from datetime import datetime -from typing import ( - Any, - Dict, - List, - NoReturn, - Tuple, -) +from typing import Optional, NoReturn, List, Tuple +from ipaddress import ip_network from urllib.parse import urlparse -from cryptography.fernet import Fernet - -from ops.charm import CharmBase, EventBase, CharmEvents -from ops.framework import StoredState, EventSource from ops.main import main -from ops.model import ( - ActiveStatus, - BlockedStatus, - # MaintenanceStatus, - WaitingStatus, - # ModelError, + +from opslib.osm.charm import CharmedOsmBase, RelationsMissing + +from opslib.osm.pod import ( + ContainerV3Builder, + PodSpecV3Builder, + FilesV3Builder, + IngressResourceV3Builder, ) -LOGGER = logging.getLogger(__name__) + +from opslib.osm.validator import ( + ModelValidator, + validator, +) + +from opslib.osm.interfaces.mysql import MysqlClient +from opslib.osm.interfaces.keystone import KeystoneServer + + +logger = logging.getLogger(__name__) + REQUIRED_SETTINGS = ["token_expiration"] @@ -46,7 +61,7 @@ REQUIRED_SETTINGS = ["token_expiration"] DATABASE_NAME = "keystone" # We expect the keystone container to use the default port -KEYSTONE_PORT = 5000 +PORT = 5000 # Number of keys need might need to be adjusted in the future NUMBER_FERNET_KEYS = 2 @@ -57,311 +72,109 @@ CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys" FERNET_KEYS_PATH = "/etc/keystone/fernet-keys" -class ConfigurePodEvent(EventBase): - """Configure Pod event""" - - pass - - -class KeystoneEvents(CharmEvents): - """Keystone Events""" - - configure_pod = EventSource(ConfigurePodEvent) - - -class KeystoneCharm(CharmBase): - """Keystone K8s Charm""" - - state = StoredState() - on = KeystoneEvents() - +class ConfigModel(ModelValidator): + region_id: str + keystone_db_password: str + admin_username: str + admin_password: str + admin_project: str + service_username: str + service_password: str + service_project: str + user_domain_name: str + project_domain_name: str + token_expiration: int + max_file_size: int + site_url: Optional[str] + ingress_whitelist_source_range: Optional[str] + tls_secret_name: Optional[str] + + @validator("max_file_size") + def validate_max_file_size(cls, v): + if v < 0: + raise ValueError("value must be equal or greater than 0") + return v + + @validator("site_url") + def validate_site_url(cls, v): + if v: + parsed = urlparse(v) + if not parsed.scheme.startswith("http"): + raise ValueError("value must start with http") + return v + + @validator("ingress_whitelist_source_range") + def validate_ingress_whitelist_source_range(cls, v): + if v: + ip_network(v) + return v + + +class ConfigLdapModel(ModelValidator): + ldap_enabled: bool + ldap_authentication_domain_name: Optional[str] + ldap_url: Optional[str] + ldap_bind_user: Optional[str] + ldap_bind_password: Optional[str] + ldap_chase_referrals: Optional[str] + ldap_page_size: Optional[int] + ldap_user_tree_dn: Optional[str] + ldap_user_objectclass: Optional[str] + ldap_user_id_attribute: Optional[str] + ldap_user_name_attribute: Optional[str] + ldap_user_pass_attribute: Optional[str] + ldap_user_filter: Optional[str] + ldap_user_enabled_attribute: Optional[str] + ldap_user_enabled_mask: Optional[int] + ldap_user_enabled_default: Optional[bool] + ldap_user_enabled_invert: Optional[bool] + ldap_group_objectclass: Optional[str] + ldap_group_tree_dn: Optional[str] + ldap_use_starttls: Optional[bool] + ldap_tls_cacert_base64: Optional[str] + ldap_tls_req_cert: Optional[str] + + +class KeystoneCharm(CharmedOsmBase): def __init__(self, *args) -> NoReturn: - """Constructor of the Charm object. - Initializes internal state and register events it can handle. - """ - super().__init__(*args) - self.state.set_default(db_host=None) - self.state.set_default(db_port=None) - self.state.set_default(db_user=None) - self.state.set_default(db_password=None) - self.state.set_default(pod_spec=None) + super().__init__(*args, oci_image="image") self.state.set_default(fernet_keys=None) self.state.set_default(credential_keys=None) self.state.set_default(keys_timestamp=0) - # Register all of the events we want to observe - self.framework.observe(self.on.config_changed, self.configure_pod) - self.framework.observe(self.on.start, self.configure_pod) - self.framework.observe(self.on.upgrade_charm, self.configure_pod) - self.framework.observe(self.on.leader_elected, self.configure_pod) - self.framework.observe(self.on.update_status, self.configure_pod) - - # Registering custom internal events - self.framework.observe(self.on.configure_pod, self.configure_pod) + self.keystone_server = KeystoneServer(self, "keystone") + self.mysql_client = MysqlClient(self, "db") + self.framework.observe(self.on["db"].relation_changed, self.configure_pod) + self.framework.observe(self.on["db"].relation_broken, self.configure_pod) - # Register relation events - self.framework.observe( - self.on.db_relation_changed, self._on_db_relation_changed - ) - self.framework.observe( - self.on.db_relation_departed, self._on_db_relation_departed - ) self.framework.observe( - self.on.keystone_relation_joined, self._publish_keystone_info + self.on["keystone"].relation_joined, self._publish_keystone_info ) - def _publish_keystone_info(self, event: EventBase) -> NoReturn: - """Publishes keystone information for NBI usage through the keystone - relation. - - Args: - event (EventBase): Keystone relation event to update NBI. - """ - config = self.model.config - rel_data = { - "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3", - "port": str(KEYSTONE_PORT), - "keystone_db_password": config["keystone_db_password"], - "region_id": config["region_id"], - "user_domain_name": config["user_domain_name"], - "project_domain_name": config["project_domain_name"], - "admin_username": config["admin_username"], - "admin_password": config["admin_password"], - "admin_project_name": config["admin_project"], - "username": config["service_username"], - "password": config["service_password"], - "service": config["service_project"], - } - for k, v in rel_data.items(): - event.relation.data[self.model.unit][k] = v - - def _on_db_relation_changed(self, event: EventBase) -> NoReturn: - """Reads information about the DB relation, in order for keystone to - access it. - - Args: - event (EventBase): DB relation event to access database - information. - """ - if event.unit not in event.relation.data: - return - relation_data = event.relation.data[event.unit] - db_host = relation_data.get("host") - db_port = int(relation_data.get("port", 3306)) - db_user = "root" - db_password = relation_data.get("root_password") - - if ( - db_host - and db_port - and db_user - and db_password - and ( - self.state.db_host != db_host - or self.state.db_port != db_port - or self.state.db_user != db_user - or self.state.db_password != db_password + def _publish_keystone_info(self, event): + if self.unit.is_leader(): + config = ConfigModel(**dict(self.config)) + self.keystone_server.publish_info( + host=f"http://{self.app.name}:{PORT}/v3", + port=PORT, + user_domain_name=config.user_domain_name, + project_domain_name=config.project_domain_name, + username=config.service_username, + password=config.service_password, + service=config.service_project, + keystone_db_password=config.keystone_db_password, + region_id=config.region_id, + admin_username=config.admin_username, + admin_password=config.admin_password, + admin_project_name=config.admin_project, ) - ): - self.state.db_host = db_host - self.state.db_port = db_port - self.state.db_user = db_user - self.state.db_password = db_password - self.on.configure_pod.emit() - - def _on_db_relation_departed(self, event: EventBase) -> NoReturn: - """Clears data from db relation. - - Args: - event (EventBase): DB relation event. - - """ - self.state.db_host = None - self.state.db_port = None - self.state.db_user = None - self.state.db_password = None - self.on.configure_pod.emit() - - def _check_settings(self) -> str: - """Check if there any settings missing from Keystone configuration. - - Returns: - str: Information about the problems found (if any). - """ - problems = [] - config = self.model.config - for setting in REQUIRED_SETTINGS: - if not config.get(setting): - problem = f"missing config {setting}" - problems.append(problem) - - return ";".join(problems) - - def _make_pod_image_details(self) -> Dict[str, str]: - """Generate the pod image details. - - Returns: - Dict[str, str]: pod image details. - """ - config = self.model.config - image_details = { - "imagePath": config["image"], - } - if config["image_username"]: - image_details.update( - { - "username": config["image_username"], - "password": config["image_password"], - } - ) - return image_details - - def _make_pod_ports(self) -> List[Dict[str, Any]]: - """Generate the pod ports details. - - Returns: - List[Dict[str, Any]]: pod ports details. - """ - return [ - {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"}, - ] - - def _make_pod_envconfig(self) -> Dict[str, Any]: - """Generate pod environment configuraiton. - - Returns: - Dict[str, Any]: pod environment configuration. - """ - config = self.model.config - - envconfig = { - "DB_HOST": self.state.db_host, - "DB_PORT": self.state.db_port, - "ROOT_DB_USER": self.state.db_user, - "ROOT_DB_PASSWORD": self.state.db_password, - "KEYSTONE_DB_PASSWORD": config["keystone_db_password"], - "REGION_ID": config["region_id"], - "KEYSTONE_HOST": self.app.name, - "ADMIN_USERNAME": config["admin_username"], - "ADMIN_PASSWORD": config["admin_password"], - "ADMIN_PROJECT": config["admin_project"], - "SERVICE_USERNAME": config["service_username"], - "SERVICE_PASSWORD": config["service_password"], - "SERVICE_PROJECT": config["service_project"], - } - - if config.get("ldap_enabled"): - envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[ - "ldap_authentication_domain_name" - ] - envconfig["LDAP_URL"] = config["ldap_url"] - envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"] - envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"] - envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"] - envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"] - envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"] - envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"] - envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"] - envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"] - envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"] - - if config["ldap_bind_user"]: - envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"] - - if config["ldap_bind_password"]: - envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"] - - if config["ldap_user_tree_dn"]: - envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"] - - if config["ldap_user_filter"]: - envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"] - - if config["ldap_user_enabled_attribute"]: - envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[ - "ldap_user_enabled_attribute" - ] - - if config["ldap_chase_referrals"]: - envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"] - - if config["ldap_group_tree_dn"]: - envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"] - - if config["ldap_use_starttls"]: - envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"] - envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"] - envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"] - - return envconfig - - def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]: - """Generate pod ingress resources. - - Returns: - List[Dict[str, Any]]: pod ingress resources. - """ - site_url = self.model.config["site_url"] - - if not site_url: - return - - parsed = urlparse(site_url) - - if not parsed.scheme.startswith("http"): - return - - max_file_size = self.model.config["max_file_size"] - ingress_whitelist_source_range = self.model.config[ - "ingress_whitelist_source_range" - ] - - annotations = { - "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size) - } - - if ingress_whitelist_source_range: - annotations[ - "nginx.ingress.kubernetes.io/whitelist-source-range" - ] = ingress_whitelist_source_range - - ingress_spec_tls = None - - if parsed.scheme == "https": - ingress_spec_tls = [{"hosts": [parsed.hostname]}] - tls_secret_name = self.model.config["tls_secret_name"] - if tls_secret_name: - ingress_spec_tls[0]["secretName"] = tls_secret_name - else: - annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" - - ingress = { - "name": "{}-ingress".format(self.app.name), - "annotations": annotations, - "spec": { - "rules": [ - { - "host": parsed.hostname, - "http": { - "paths": [ - { - "path": "/", - "backend": { - "serviceName": self.app.name, - "servicePort": KEYSTONE_PORT, - }, - } - ] - }, - } - ], - }, - } - if ingress_spec_tls: - ingress["spec"]["tls"] = ingress_spec_tls - - return [ingress] + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] + if self.mysql_client.is_missing_data_in_unit(): + missing_relations.append("mysql") + if missing_relations: + raise RelationsMissing(missing_relations) def _generate_keys(self) -> Tuple[List[str], List[str]]: """Generating new fernet tokens. @@ -382,58 +195,8 @@ class KeystoneCharm(CharmBase): return (fernet_keys, credential_keys) - def _make_pod_files( - self, fernet_keys: List[str], credential_keys: List[str] - ) -> List[Dict[str, Any]]: - """Generating ConfigMap information. - - Args: - fernet_keys (List[str]): keys for fernet. - credential_keys (List[str]): keys for credentials. - - Returns: - List[Dict[str, Any]]: ConfigMap information. - """ - files = [ - { - "name": "fernet-keys", - "mountPath": FERNET_KEYS_PATH, - "files": [ - {"path": str(key_id), "content": value} - for (key_id, value) in enumerate(fernet_keys) - ], - } - ] - - files.append( - { - "name": "credential-keys", - "mountPath": CREDENTIAL_KEYS_PATH, - "files": [ - {"path": str(key_id), "content": value} - for (key_id, value) in enumerate(credential_keys) - ], - } - ) - - return files - - def configure_pod(self, event: EventBase) -> NoReturn: - """Assemble the pod spec and apply it, if possible. - - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if not self.state.db_host: - self.unit.status = WaitingStatus("Waiting for database relation") - event.defer() - return - - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - return - + def _get_keys(self): + keys_timestamp = self.state.keys_timestamp if fernet_keys := self.state.fernet_keys: fernet_keys = json.loads(fernet_keys) @@ -441,8 +204,7 @@ class KeystoneCharm(CharmBase): credential_keys = json.loads(credential_keys) now = datetime.now().timestamp() - keys_timestamp = self.state.keys_timestamp - token_expiration = self.model.config["token_expiration"] + token_expiration = self.config["token_expiration"] valid_keys = (now - keys_timestamp) < token_expiration if not credential_keys or not fernet_keys or not valid_keys: @@ -450,42 +212,553 @@ class KeystoneCharm(CharmBase): self.state.fernet_keys = json.dumps(fernet_keys) self.state.credential_keys = json.dumps(credential_keys) self.state.keys_timestamp = now + return credential_keys, fernet_keys + + def _build_files(self, config: ConfigModel): + credentials_files_builder = FilesV3Builder() + fernet_files_builder = FilesV3Builder() + + credential_keys, fernet_keys = self._get_keys() + + for (key_id, value) in enumerate(credential_keys): + credentials_files_builder.add_file(str(key_id), value) + for (key_id, value) in enumerate(fernet_keys): + fernet_files_builder.add_file(str(key_id), value) + return credentials_files_builder.build(), fernet_files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + config_ldap = ConfigLdapModel(**dict(self.config)) + # Check relations + self._check_missing_dependencies(config) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + # Build Container + container_builder = ContainerV3Builder(self.app.name, image_info) + container_builder.add_port(name=self.app.name, port=PORT) + # Build files + credential_files, fernet_files = self._build_files(config) + container_builder.add_volume_config( + "credential-keys", CREDENTIAL_KEYS_PATH, credential_files + ) + container_builder.add_volume_config( + "fernet-keys", FERNET_KEYS_PATH, fernet_files + ) + container_builder.add_envs( + { + "DB_HOST": self.mysql_client.host, + "DB_PORT": self.mysql_client.port, + "ROOT_DB_USER": "root", + "ROOT_DB_PASSWORD": self.mysql_client.root_password, + "KEYSTONE_DB_PASSWORD": config.keystone_db_password, + "REGION_ID": config.region_id, + "KEYSTONE_HOST": self.app.name, + "ADMIN_USERNAME": config.admin_username, + "ADMIN_PASSWORD": config.admin_password, + "ADMIN_PROJECT": config.admin_project, + "SERVICE_USERNAME": config.service_username, + "SERVICE_PASSWORD": config.service_password, + "SERVICE_PROJECT": config.service_project, + } + ) - # Check problems in the settings - problems = self._check_settings() - if problems: - self.unit.status = BlockedStatus(problems) - return - - self.unit.status = BlockedStatus("Assembling pod spec") - image_details = self._make_pod_image_details() - ports = self._make_pod_ports() - env_config = self._make_pod_envconfig() - ingress_resources = self._make_pod_ingress_resources() - files = self._make_pod_files(fernet_keys, credential_keys) - - pod_spec = { - "version": 3, - "containers": [ + if config_ldap.ldap_enabled: + + container_builder.add_envs( { - "name": self.framework.model.app.name, - "imageDetails": image_details, - "ports": ports, - "envConfig": env_config, - "volumeConfig": files, + "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name, + "LDAP_URL": config_ldap.ldap_url, + "LDAP_PAGE_SIZE": config_ldap.ldap_page_size, + "LDAP_USER_OBJECTCLASS": config_ldap.ldap_user_objectclass, + "LDAP_USER_ID_ATTRIBUTE": config_ldap.ldap_user_id_attribute, + "LDAP_USER_NAME_ATTRIBUTE": config_ldap.ldap_user_name_attribute, + "LDAP_USER_PASS_ATTRIBUTE": config_ldap.ldap_user_pass_attribute, + "LDAP_USER_ENABLED_MASK": config_ldap.ldap_user_enabled_mask, + "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default, + "LDAP_USER_ENABLED_INVERT": config_ldap.ldap_user_enabled_invert, + "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass, } - ], - "kubernetesResources": {"ingressResources": ingress_resources or []}, - } + ) + if config_ldap.ldap_bind_user: + container_builder.add_envs( + {"LDAP_BIND_USER": config_ldap.ldap_bind_user} + ) + + if config_ldap.ldap_bind_password: + container_builder.add_envs( + {"LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password} + ) + + if config_ldap.ldap_user_tree_dn: + container_builder.add_envs( + {"LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn} + ) + + if config_ldap.ldap_user_filter: + container_builder.add_envs( + {"LDAP_USER_FILTER": config_ldap.ldap_user_filter} + ) + + if config_ldap.ldap_user_enabled_attribute: + container_builder.add_envs( + { + "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute + } + ) - if self.state.pod_spec != ( - pod_spec_json := json.dumps(pod_spec, sort_keys=True) - ): - self.state.pod_spec = pod_spec_json - self.model.pod.set_spec(pod_spec) + if config_ldap.ldap_chase_referrals: + container_builder.add_envs( + {"LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals} + ) - self.unit.status = ActiveStatus("ready") + if config_ldap.ldap_group_tree_dn: + container_builder.add_envs( + {"LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn} + ) + + if config_ldap.ldap_use_starttls: + container_builder.add_envs( + { + "LDAP_USE_STARTTLS": config_ldap.ldap_use_starttls, + "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64, + "LDAP_TLS_REQ_CERT": config_ldap.ldap_tls_req_cert, + } + ) + container = container_builder.build() + # Add container to pod spec + pod_spec_builder.add_container(container) + # Add ingress resources to pod spec if site url exists + if config.site_url: + parsed = urlparse(config.site_url) + annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( + str(config.max_file_size) + "m" + if config.max_file_size > 0 + else config.max_file_size + ), + } + ingress_resource_builder = IngressResourceV3Builder( + f"{self.app.name}-ingress", annotations + ) + + if config.ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = config.ingress_whitelist_source_range + + if parsed.scheme == "https": + ingress_resource_builder.add_tls( + [parsed.hostname], config.tls_secret_name + ) + else: + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + + ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) + ingress_resource = ingress_resource_builder.build() + pod_spec_builder.add_ingress_resource(ingress_resource) + return pod_spec_builder.build() if __name__ == "__main__": main(KeystoneCharm) + +# LOGGER = logging.getLogger(__name__) + + +# class ConfigurePodEvent(EventBase): +# """Configure Pod event""" + +# pass + + +# class KeystoneEvents(CharmEvents): +# """Keystone Events""" + +# configure_pod = EventSource(ConfigurePodEvent) + +# class KeystoneCharm(CharmBase): +# """Keystone K8s Charm""" + +# state = StoredState() +# on = KeystoneEvents() + +# def __init__(self, *args) -> NoReturn: +# """Constructor of the Charm object. +# Initializes internal state and register events it can handle. +# """ +# super().__init__(*args) +# self.state.set_default(db_host=None) +# self.state.set_default(db_port=None) +# self.state.set_default(db_user=None) +# self.state.set_default(db_password=None) +# self.state.set_default(pod_spec=None) +# self.state.set_default(fernet_keys=None) +# self.state.set_default(credential_keys=None) +# self.state.set_default(keys_timestamp=0) + +# # Register all of the events we want to observe +# self.framework.observe(self.on.config_changed, self.configure_pod) +# self.framework.observe(self.on.start, self.configure_pod) +# self.framework.observe(self.on.upgrade_charm, self.configure_pod) +# self.framework.observe(self.on.leader_elected, self.configure_pod) +# self.framework.observe(self.on.update_status, self.configure_pod) + +# # Registering custom internal events +# self.framework.observe(self.on.configure_pod, self.configure_pod) + +# # Register relation events +# self.framework.observe( +# self.on.db_relation_changed, self._on_db_relation_changed +# ) +# self.framework.observe( +# self.on.db_relation_broken, self._on_db_relation_broken +# ) +# self.framework.observe( +# self.on.keystone_relation_joined, self._publish_keystone_info +# ) + +# def _publish_keystone_info(self, event: EventBase) -> NoReturn: +# """Publishes keystone information for NBI usage through the keystone +# relation. + +# Args: +# event (EventBase): Keystone relation event to update NBI. +# """ +# config = self.model.config +# rel_data = { +# "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3", +# "port": str(KEYSTONE_PORT), +# "keystone_db_password": config["keystone_db_password"], +# "region_id": config["region_id"], +# "user_domain_name": config["user_domain_name"], +# "project_domain_name": config["project_domain_name"], +# "admin_username": config["admin_username"], +# "admin_password": config["admin_password"], +# "admin_project_name": config["admin_project"], +# "username": config["service_username"], +# "password": config["service_password"], +# "service": config["service_project"], +# } +# for k, v in rel_data.items(): +# event.relation.data[self.model.unit][k] = v + +# def _on_db_relation_changed(self, event: EventBase) -> NoReturn: +# """Reads information about the DB relation, in order for keystone to +# access it. + +# Args: +# event (EventBase): DB relation event to access database +# information. +# """ +# if not event.unit in event.relation.data: +# return +# relation_data = event.relation.data[event.unit] +# db_host = relation_data.get("host") +# db_port = int(relation_data.get("port", 3306)) +# db_user = "root" +# db_password = relation_data.get("root_password") + +# if ( +# db_host +# and db_port +# and db_user +# and db_password +# and ( +# self.state.db_host != db_host +# or self.state.db_port != db_port +# or self.state.db_user != db_user +# or self.state.db_password != db_password +# ) +# ): +# self.state.db_host = db_host +# self.state.db_port = db_port +# self.state.db_user = db_user +# self.state.db_password = db_password +# self.on.configure_pod.emit() + + +# def _on_db_relation_broken(self, event: EventBase) -> NoReturn: +# """Clears data from db relation. + +# Args: +# event (EventBase): DB relation event. + +# """ +# self.state.db_host = None +# self.state.db_port = None +# self.state.db_user = None +# self.state.db_password = None +# self.on.configure_pod.emit() + +# def _check_settings(self) -> str: +# """Check if there any settings missing from Keystone configuration. + +# Returns: +# str: Information about the problems found (if any). +# """ +# problems = [] +# config = self.model.config + +# for setting in REQUIRED_SETTINGS: +# if not config.get(setting): +# problem = f"missing config {setting}" +# problems.append(problem) + +# return ";".join(problems) + +# def _make_pod_image_details(self) -> Dict[str, str]: +# """Generate the pod image details. + +# Returns: +# Dict[str, str]: pod image details. +# """ +# config = self.model.config +# image_details = { +# "imagePath": config["image"], +# } +# if config["image_username"]: +# image_details.update( +# { +# "username": config["image_username"], +# "password": config["image_password"], +# } +# ) +# return image_details + +# def _make_pod_ports(self) -> List[Dict[str, Any]]: +# """Generate the pod ports details. + +# Returns: +# List[Dict[str, Any]]: pod ports details. +# """ +# return [ +# {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"}, +# ] + +# def _make_pod_envconfig(self) -> Dict[str, Any]: +# """Generate pod environment configuraiton. + +# Returns: +# Dict[str, Any]: pod environment configuration. +# """ +# config = self.model.config + +# envconfig = { +# "DB_HOST": self.state.db_host, +# "DB_PORT": self.state.db_port, +# "ROOT_DB_USER": self.state.db_user, +# "ROOT_DB_PASSWORD": self.state.db_password, +# "KEYSTONE_DB_PASSWORD": config["keystone_db_password"], +# "REGION_ID": config["region_id"], +# "KEYSTONE_HOST": self.app.name, +# "ADMIN_USERNAME": config["admin_username"], +# "ADMIN_PASSWORD": config["admin_password"], +# "ADMIN_PROJECT": config["admin_project"], +# "SERVICE_USERNAME": config["service_username"], +# "SERVICE_PASSWORD": config["service_password"], +# "SERVICE_PROJECT": config["service_project"], +# } + +# if config.get("ldap_enabled"): +# envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[ +# "ldap_authentication_domain_name" +# ] +# envconfig["LDAP_URL"] = config["ldap_url"] +# envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"] +# envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"] +# envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"] +# envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"] +# envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"] +# envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"] +# envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"] +# envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"] +# envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"] + +# if config["ldap_bind_user"]: +# envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"] + +# if config["ldap_bind_password"]: +# envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"] + +# if config["ldap_user_tree_dn"]: +# envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"] + +# if config["ldap_user_filter"]: +# envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"] + +# if config["ldap_user_enabled_attribute"]: +# envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[ +# "ldap_user_enabled_attribute" +# ] + +# if config["ldap_chase_referrals"]: +# envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"] + +# if config["ldap_group_tree_dn"]: +# envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"] + +# if config["ldap_use_starttls"]: +# envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"] +# envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"] +# envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"] + +# return envconfig + +# def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]: +# """Generate pod ingress resources. + +# Returns: +# List[Dict[str, Any]]: pod ingress resources. +# """ +# site_url = self.model.config["site_url"] + +# if not site_url: +# return + +# parsed = urlparse(site_url) + +# if not parsed.scheme.startswith("http"): +# return + +# max_file_size = self.model.config["max_file_size"] +# ingress_whitelist_source_range = self.model.config[ +# "ingress_whitelist_source_range" +# ] + +# annotations = { +# "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size) +# } + +# if ingress_whitelist_source_range: +# annotations[ +# "nginx.ingress.kubernetes.io/whitelist-source-range" +# ] = ingress_whitelist_source_range + +# ingress_spec_tls = None + +# if parsed.scheme == "https": +# ingress_spec_tls = [{"hosts": [parsed.hostname]}] +# tls_secret_name = self.model.config["tls_secret_name"] +# if tls_secret_name: +# ingress_spec_tls[0]["secretName"] = tls_secret_name +# else: +# annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + +# ingress = { +# "name": "{}-ingress".format(self.app.name), +# "annotations": annotations, +# "spec": { +# "rules": [ +# { +# "host": parsed.hostname, +# "http": { +# "paths": [ +# { +# "path": "/", +# "backend": { +# "serviceName": self.app.name, +# "servicePort": KEYSTONE_PORT, +# }, +# } +# ] +# }, +# } +# ], +# }, +# } +# if ingress_spec_tls: +# ingress["spec"]["tls"] = ingress_spec_tls + +# return [ingress] + +# def _generate_keys(self) -> Tuple[List[str], List[str]]: +# """Generating new fernet tokens. + +# Returns: +# Tuple[List[str], List[str]]: contains two lists of strings. First +# list contains strings that represent +# the keys for fernet and the second +# list contains strins that represent +# the keys for credentials. +# """ +# fernet_keys = [ +# Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS) +# ] +# credential_keys = [ +# Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS) +# ] + +# return (fernet_keys, credential_keys) + +# def configure_pod(self, event: EventBase) -> NoReturn: +# """Assemble the pod spec and apply it, if possible. + +# Args: +# event (EventBase): Hook or Relation event that started the +# function. +# """ +# if not self.state.db_host: +# self.unit.status = WaitingStatus("Waiting for database relation") +# event.defer() +# return + +# if not self.unit.is_leader(): +# self.unit.status = ActiveStatus("ready") +# return + +# if fernet_keys := self.state.fernet_keys: +# fernet_keys = json.loads(fernet_keys) + +# if credential_keys := self.state.credential_keys: +# credential_keys = json.loads(credential_keys) + +# now = datetime.now().timestamp() +# keys_timestamp = self.state.keys_timestamp +# token_expiration = self.model.config["token_expiration"] + +# valid_keys = (now - keys_timestamp) < token_expiration +# if not credential_keys or not fernet_keys or not valid_keys: +# fernet_keys, credential_keys = self._generate_keys() +# self.state.fernet_keys = json.dumps(fernet_keys) +# self.state.credential_keys = json.dumps(credential_keys) +# self.state.keys_timestamp = now + +# # Check problems in the settings +# problems = self._check_settings() +# if problems: +# self.unit.status = BlockedStatus(problems) +# return + +# self.unit.status = BlockedStatus("Assembling pod spec") +# image_details = self._make_pod_image_details() +# ports = self._make_pod_ports() +# env_config = self._make_pod_envconfig() +# ingress_resources = self._make_pod_ingress_resources() +# files = self._make_pod_files(fernet_keys, credential_keys) + +# pod_spec = { +# "version": 3, +# "containers": [ +# { +# "name": self.framework.model.app.name, +# "imageDetails": image_details, +# "ports": ports, +# "envConfig": env_config, +# "volumeConfig": files, +# } +# ], +# "kubernetesResources": {"ingressResources": ingress_resources or []}, +# } + +# if self.state.pod_spec != ( +# pod_spec_json := json.dumps(pod_spec, sort_keys=True) +# ): +# self.state.pod_spec = pod_spec_json +# self.model.pod.set_spec(pod_spec) + +# self.unit.status = ActiveStatus("ready") + + +# if __name__ == "__main__": +# main(KeystoneCharm)