Revert "Feature 11071: Modular OSM installation. Remove charms, juju and lxd"

This reverts commit a0f0d8ef4f2aa0dd227ecb651002490b66498bab.

Change-Id: I92394e4074dad4e457c107c58e4ebc17d507f8b2
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
diff --git a/installers/charm/osm-keystone/src/charm.py b/installers/charm/osm-keystone/src/charm.py
new file mode 100755
index 0000000..c368ade
--- /dev/null
+++ b/installers/charm/osm-keystone/src/charm.py
@@ -0,0 +1,443 @@
+#!/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
+#
+#
+# This file populates the Actions tab on Charmhub.
+# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
+
+"""Keystone charm module."""
+
+import logging
+from datetime import datetime
+
+from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
+from config_validator import ValidationError
+from ops import pebble
+from ops.charm import ActionEvent, CharmBase, ConfigChangedEvent, UpdateStatusEvent
+from ops.main import main
+from ops.model import ActiveStatus, BlockedStatus, Container, MaintenanceStatus
+
+import cluster
+from config import ConfigModel, MysqlConnectionData, get_environment, validate_config
+from interfaces import KeystoneServer, MysqlClient
+
+logger = logging.getLogger(__name__)
+
+
+# We expect the keystone container to use the default port
+PORT = 5000
+
+KEY_SETUP_FILE = "/etc/keystone/key-setup"
+CREDENTIAL_KEY_REPOSITORY = "/etc/keystone/credential-keys/"
+FERNET_KEY_REPOSITORY = "/etc/keystone/fernet-keys/"
+KEYSTONE_USER = "keystone"
+KEYSTONE_GROUP = "keystone"
+FERNET_MAX_ACTIVE_KEYS = 3
+KEYSTONE_FOLDER = "/etc/keystone/"
+
+
+class CharmError(Exception):
+    """Charm error exception."""
+
+
+class KeystoneCharm(CharmBase):
+    """Keystone Charm operator."""
+
+    on = cluster.ClusterEvents()
+
+    def __init__(self, *args) -> None:
+        super().__init__(*args)
+        event_observe_mapping = {
+            self.on.keystone_pebble_ready: self._on_config_changed,
+            self.on.config_changed: self._on_config_changed,
+            self.on.update_status: self._on_update_status,
+            self.on.cluster_keys_changed: self._on_cluster_keys_changed,
+            self.on["keystone"].relation_joined: self._publish_keystone_info,
+            self.on["db"].relation_changed: self._on_config_changed,
+            self.on["db"].relation_broken: self._on_config_changed,
+            self.on["db-sync"].action: self._on_db_sync_action,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+        self.cluster = cluster.Cluster(self)
+        self.mysql_client = MysqlClient(self, relation_name="db")
+        self.keystone = KeystoneServer(self, relation_name="keystone")
+        self.service_patch = KubernetesServicePatch(self, [(f"{self.app.name}", PORT)])
+
+    @property
+    def container(self) -> Container:
+        """Property to get keystone container."""
+        return self.unit.get_container("keystone")
+
+    def _on_db_sync_action(self, event: ActionEvent):
+        process = self.container.exec(["keystone-manage", "db_sync"])
+        try:
+            process.wait()
+            event.set_results({"output": "db-sync was successfully executed."})
+        except pebble.ExecError as e:
+            error_message = f"db-sync action failed with code {e.exit_code} and stderr {e.stderr}."
+            logger.error(error_message)
+            event.fail(error_message)
+
+    def _publish_keystone_info(self, _):
+        """Handler for keystone-relation-joined."""
+        if self.unit.is_leader():
+            config = ConfigModel(**dict(self.config))
+            self.keystone.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,
+            )
+
+    def _on_config_changed(self, _: ConfigChangedEvent) -> None:
+        """Handler for config-changed event."""
+        if self.container.can_connect():
+            try:
+                self._handle_fernet_key_rotation()
+                self._safe_restart()
+                self.unit.status = ActiveStatus()
+            except CharmError as e:
+                self.unit.status = BlockedStatus(str(e))
+            except ValidationError as e:
+                self.unit.status = BlockedStatus(str(e))
+        else:
+            logger.info("pebble socket not available, deferring config-changed")
+            self.unit.status = MaintenanceStatus("waiting for pebble to start")
+
+    def _on_update_status(self, event: UpdateStatusEvent) -> None:
+        """Handler for update-status event."""
+        if self.container.can_connect():
+            self._handle_fernet_key_rotation()
+        else:
+            logger.info("pebble socket not available, deferring config-changed")
+            event.defer()
+            self.unit.status = MaintenanceStatus("waiting for pebble to start")
+
+    def _on_cluster_keys_changed(self, _) -> None:
+        """Handler for ClusterKeysChanged event."""
+        self._handle_fernet_key_rotation()
+
+    def _handle_fernet_key_rotation(self) -> None:
+        """Handles fernet key rotation.
+
+        First, the function writes the existing keys in the relation to disk.
+        Then, if the unit is the leader, checks if the keys should be rotated
+        or not.
+        """
+        self._key_write()
+        if self.unit.is_leader():
+            if not self.cluster.get_keys():
+                self._key_setup()
+            self._fernet_keys_rotate_and_sync()
+
+    def _key_write(self) -> None:
+        """Write keys to container from the relation data."""
+        if self.unit.is_leader():
+            return
+        keys = self.cluster.get_keys()
+        if not keys:
+            logger.debug('"key_repository" not in relation data yet...')
+            return
+
+        for key_repository in [FERNET_KEY_REPOSITORY, CREDENTIAL_KEY_REPOSITORY]:
+            self._create_keys_folders()
+            for key_number, key in keys[key_repository].items():
+                logger.debug(f"writing key {key_number} in {key_repository}")
+                file_path = f"{key_repository}{key_number}"
+                if self._file_changed(file_path, key):
+                    self.container.push(
+                        file_path,
+                        key,
+                        user=KEYSTONE_USER,
+                        group=KEYSTONE_GROUP,
+                        permissions=0o600,
+                    )
+        self.container.push(KEY_SETUP_FILE, "")
+
+    def _file_changed(self, file_path: str, content: str) -> bool:
+        """Check if file in container has changed its value.
+
+        This function checks if the file exists in the container. If it does,
+        then it checks if the content of that file is equal to the content passed to
+        this function. If the content is equal, the function returns False, otherwise True.
+
+        Args:
+            file_path (str): File path in the container.
+            content (str): Content of the file.
+
+        Returns:
+            bool: True if the content of the file has changed, or the file doesn't exist in
+                  the container. False if the content passed to this function is the same as
+                  in the container.
+        """
+        if self._file_exists(file_path):
+            old_content = self.container.pull(file_path).read()
+            if old_content == content:
+                return False
+        return True
+
+    def _create_keys_folders(self) -> None:
+        """Create folders for Key repositories."""
+        fernet_key_repository_found = False
+        credential_key_repository_found = False
+        for file in self.container.list_files(KEYSTONE_FOLDER):
+            if file.type == pebble.FileType.DIRECTORY:
+                if file.path == CREDENTIAL_KEY_REPOSITORY:
+                    credential_key_repository_found = True
+                if file.path == FERNET_KEY_REPOSITORY:
+                    fernet_key_repository_found = True
+        if not fernet_key_repository_found:
+            self.container.make_dir(
+                FERNET_KEY_REPOSITORY,
+                user="keystone",
+                group="keystone",
+                permissions=0o700,
+                make_parents=True,
+            )
+        if not credential_key_repository_found:
+            self.container.make_dir(
+                CREDENTIAL_KEY_REPOSITORY,
+                user=KEYSTONE_USER,
+                group=KEYSTONE_GROUP,
+                permissions=0o700,
+                make_parents=True,
+            )
+
+    def _fernet_keys_rotate_and_sync(self) -> None:
+        """Rotate and sync the keys if the unit is the leader and the primary key has expired.
+
+        The modification time of the staging key (key with index '0') is used,
+        along with the config setting "token-expiration" to determine whether to
+        rotate the keys.
+
+        The rotation time = token-expiration / (max-active-keys - 2)
+        where max-active-keys has a minimum of 3.
+        """
+        if not self.unit.is_leader():
+            return
+        try:
+            fernet_key_file = self.container.list_files(f"{FERNET_KEY_REPOSITORY}0")[0]
+            last_rotation = fernet_key_file.last_modified.timestamp()
+        except pebble.APIError:
+            logger.warning(
+                "Fernet key rotation requested but key repository not " "initialized yet"
+            )
+            return
+
+        config = ConfigModel(**self.config)
+        rotation_time = config.token_expiration // (FERNET_MAX_ACTIVE_KEYS - 2)
+
+        now = datetime.now().timestamp()
+        if last_rotation + rotation_time > now:
+            # No rotation to do as not reached rotation time
+            logger.debug("No rotation needed")
+            self._key_leader_set()
+            return
+        # now rotate the keys and sync them
+        self._fernet_rotate()
+        self._key_leader_set()
+
+        logger.info("Rotated and started sync of fernet keys")
+
+    def _key_leader_set(self) -> None:
+        """Read current key sets and update peer relation data.
+
+        The keys are read from the `FERNET_KEY_REPOSITORY` and `CREDENTIAL_KEY_REPOSITORY`
+        directories. Note that this function will fail if it is called on the unit that is
+        not the leader.
+        """
+        disk_keys = {}
+        for key_repository in [FERNET_KEY_REPOSITORY, CREDENTIAL_KEY_REPOSITORY]:
+            disk_keys[key_repository] = {}
+            for file in self.container.list_files(key_repository):
+                key_content = self.container.pull(f"{key_repository}{file.name}").read()
+                disk_keys[key_repository][file.name] = key_content
+        self.cluster.save_keys(disk_keys)
+
+    def _fernet_rotate(self) -> None:
+        """Rotate Fernet keys.
+
+        To rotate the Fernet tokens, and create a new staging key, it calls (as the
+        "keystone" user):
+
+            keystone-manage fernet_rotate
+
+        Note that we do not rotate the Credential encryption keys.
+
+        Note that this does NOT synchronise the keys between the units.  This is
+        performed in `self._key_leader_set`.
+        """
+        logger.debug("Rotating Fernet tokens")
+        try:
+            exec_command = [
+                "keystone-manage",
+                "fernet_rotate",
+                "--keystone-user",
+                KEYSTONE_USER,
+                "--keystone-group",
+                KEYSTONE_GROUP,
+            ]
+            logger.debug(f'Executing command: {" ".join(exec_command)}')
+            self.container.exec(exec_command).wait()
+            logger.info("Fernet keys successfully rotated.")
+        except pebble.ExecError as e:
+            logger.error(f"Fernet Key rotation failed: {e}")
+            logger.error("Exited with code %d. Stderr:", e.exit_code)
+            for line in e.stderr.splitlines():
+                logger.error("    %s", line)
+
+    def _key_setup(self) -> None:
+        """Initialize Fernet and Credential encryption key repositories.
+
+        To setup the key repositories:
+
+            keystone-manage fernet_setup
+            keystone-manage credential_setup
+
+        In addition we migrate any credentials currently stored in database using
+        the null key to be encrypted by the new credential key:
+
+            keystone-manage credential_migrate
+
+        Note that we only want to do this once, so we touch an empty file
+        (KEY_SETUP_FILE) to indicate that it has been done.
+        """
+        if self._file_exists(KEY_SETUP_FILE) or not self.unit.is_leader():
+            return
+
+        logger.debug("Setting up key repositories for Fernet tokens and Credential encryption.")
+        try:
+            for command in ["fernet_setup", "credential_setup"]:
+                exec_command = [
+                    "keystone-manage",
+                    command,
+                    "--keystone-user",
+                    KEYSTONE_USER,
+                    "--keystone-group",
+                    KEYSTONE_GROUP,
+                ]
+                logger.debug(f'Executing command: {" ".join(exec_command)}')
+                self.container.exec(exec_command).wait()
+            self.container.push(KEY_SETUP_FILE, "")
+            logger.info("Key repositories initialized successfully.")
+        except pebble.ExecError as e:
+            logger.error("Failed initializing key repositories.")
+            logger.error("Exited with code %d. Stderr:", e.exit_code)
+            for line in e.stderr.splitlines():
+                logger.error("    %s", line)
+
+    def _file_exists(self, path: str) -> bool:
+        """Check if a file exists in the container.
+
+        Args:
+            path (str): Path of the file to be checked.
+
+        Returns:
+            bool: True if the file exists, else False.
+        """
+        file_exists = None
+        try:
+            _ = self.container.pull(path)
+            file_exists = True
+        except pebble.PathError:
+            file_exists = False
+        exist_str = "exists" if file_exists else 'doesn"t exist'
+        logger.debug(f"File {path} {exist_str}.")
+        return file_exists
+
+    def _safe_restart(self) -> None:
+        """Safely restart the keystone service.
+
+        This function (re)starts the keystone service after doing some safety checks,
+        like validating the charm configuration, checking the mysql relation is ready.
+        """
+        validate_config(self.config)
+        self._check_mysql_data()
+        # Workaround: OS_AUTH_URL is not ready when the entrypoint restarts apache2.
+        # The function `self._patch_entrypoint` fixes that.
+        self._patch_entrypoint()
+        self._replan()
+
+    def _patch_entrypoint(self) -> None:
+        """Patches the entrypoint of the Keystone service.
+
+        The entrypoint that restarts apache2, expects immediate communication to OS_AUTH_URL.
+        This does not happen instantly. This function patches the entrypoint to wait until a
+        curl to OS_AUTH_URL succeeds.
+        """
+        installer_script = self.container.pull("/app/start.sh").read()
+        wait_until_ready_command = "until $(curl --output /dev/null --silent --head --fail $OS_AUTH_URL); do echo '...'; sleep 5; done"
+        self.container.push(
+            "/app/start-patched.sh",
+            installer_script.replace(
+                "source setup_env", f"source setup_env && {wait_until_ready_command}"
+            ),
+            permissions=0o755,
+        )
+
+    def _check_mysql_data(self) -> None:
+        """Check if the mysql relation is ready.
+
+        Raises:
+            CharmError: Error raised if the mysql relation is not ready.
+        """
+        if self.mysql_client.is_missing_data_in_unit() and not self.config.get("mysql-uri"):
+            raise CharmError("mysql relation is missing")
+
+    def _replan(self) -> None:
+        """Replan keystone service.
+
+        This function starts the keystone service if it is not running.
+        If the service started already, this function will restart the
+        service if there are any changes to the layer.
+        """
+        mysql_data = MysqlConnectionData(
+            self.config.get("mysql-uri")
+            or f"mysql://root:{self.mysql_client.root_password}@{self.mysql_client.host}:{self.mysql_client.port}/"
+        )
+        layer = {
+            "summary": "keystone layer",
+            "description": "pebble config layer for keystone",
+            "services": {
+                "keystone": {
+                    "override": "replace",
+                    "summary": "keystone service",
+                    "command": "/app/start-patched.sh",
+                    "startup": "enabled",
+                    "environment": get_environment(self.app.name, self.config, mysql_data),
+                }
+            },
+        }
+        self.container.add_layer("keystone", layer, combine=True)
+        self.container.replan()
+
+
+if __name__ == "__main__":  # pragma: no cover
+    main(KeystoneCharm)
diff --git a/installers/charm/osm-keystone/src/cluster.py b/installers/charm/osm-keystone/src/cluster.py
new file mode 100644
index 0000000..f38adec
--- /dev/null
+++ b/installers/charm/osm-keystone/src/cluster.py
@@ -0,0 +1,135 @@
+# 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
+#
+#
+# This file populates the Actions tab on Charmhub.
+# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
+
+"""Keystone cluster library.
+
+This library allows the integration with Apache Guacd charm. Is is published as part of the
+[davigar15-apache-guacd]((https://charmhub.io/davigar15-apache-guacd) charm.
+
+The charm that requires guacd should include the following content in its metadata.yaml:
+
+```yaml
+# ...
+peers:
+    cluster:
+        interface: cluster
+# ...
+```
+
+A typical example of including this library might be:
+
+```python
+# ...
+from ops.framework import StoredState
+from charms.keystone.v0 import cluster
+
+class SomeApplication(CharmBase):
+  on = cluster.ClusterEvents()
+
+  def __init__(self, *args):
+    # ...
+    self.cluster = cluster.Cluster(self)
+    self.framework.observe(self.on.cluster_keys_changed, self._cluster_keys_changed)
+    # ...
+
+  def _cluster_keys_changed(self, _):
+    fernet_keys = self.cluster.fernet_keys
+    credential_keys = self.cluster.credential_keys
+    # ...
+```
+"""
+
+
+import json
+import logging
+from typing import Any, Dict, List
+
+from ops.charm import CharmEvents
+from ops.framework import EventBase, EventSource, Object
+from ops.model import Relation
+
+# Number of keys need might need to be adjusted in the future
+NUMBER_FERNET_KEYS = 2
+NUMBER_CREDENTIAL_KEYS = 2
+
+logger = logging.getLogger(__name__)
+
+
+class ClusterKeysChangedEvent(EventBase):
+    """Event to announce a change in the Guacd service."""
+
+
+class ClusterEvents(CharmEvents):
+    """Cluster Events."""
+
+    cluster_keys_changed = EventSource(ClusterKeysChangedEvent)
+
+
+class Cluster(Object):
+    """Peer relation."""
+
+    def __init__(self, charm):
+        super().__init__(charm, "cluster")
+        self.charm = charm
+
+    @property
+    def fernet_keys(self) -> List[str]:
+        """Fernet keys."""
+        relation: Relation = self.model.get_relation("cluster")
+        application_data = relation.data[self.model.app]
+        return json.loads(application_data.get("keys-fernet", "[]"))
+
+    @property
+    def credential_keys(self) -> List[str]:
+        """Credential keys."""
+        relation: Relation = self.model.get_relation("cluster")
+        application_data = relation.data[self.model.app]
+        return json.loads(application_data.get("keys-credential", "[]"))
+
+    def save_keys(self, keys: Dict[str, Any]) -> None:
+        """Generate fernet and credential keys.
+
+        This method will generate new keys and fire the cluster_keys_changed event.
+        """
+        logger.debug("Saving keys...")
+        relation: Relation = self.model.get_relation("cluster")
+        data = relation.data[self.model.app]
+        current_keys_str = data.get("key_repository", "{}")
+        current_keys = json.loads(current_keys_str)
+        if current_keys != keys:
+            data["key_repository"] = json.dumps(keys)
+            self.charm.on.cluster_keys_changed.emit()
+        logger.info("Keys saved!")
+
+    def get_keys(self) -> Dict[str, Any]:
+        """Get keys from the relation.
+
+        Returns:
+            Dict[str, Any]: Dictionary with the keys.
+        """
+        relation: Relation = self.model.get_relation("cluster")
+        data = relation.data[self.model.app]
+        current_keys_str = data.get("key_repository", "{}")
+        current_keys = json.loads(current_keys_str)
+        return current_keys
diff --git a/installers/charm/osm-keystone/src/config.py b/installers/charm/osm-keystone/src/config.py
new file mode 100644
index 0000000..803d564
--- /dev/null
+++ b/installers/charm/osm-keystone/src/config.py
@@ -0,0 +1,184 @@
+#!/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
+#
+#
+# This file populates the Actions tab on Charmhub.
+# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
+
+"""Module that takes take of the charm configuration."""
+
+import re
+from typing import Any, Dict, Optional
+
+from config_validator import ConfigValidator, ValidationError
+from ops.model import ConfigData
+
+
+class MysqlConnectionData:
+    """Mysql Connection Data class."""
+
+    _compiled_regex = re.compile(
+        r"^mysql\:\/\/{}@{}\/{}?$".format(
+            r"(?P<username>[_\w]+):(?P<password>[\w\W]+)",
+            r"(?P<host>[\-\.\w]+):(?P<port>\d+)",
+            r"(?P<database>[_\w]+)",
+        )
+    )
+
+    def __init__(self, mysql_uri: str):
+        match = self._compiled_regex.search(mysql_uri)
+        if not match:
+            raise ValidationError("mysql_uri is not properly formed")
+        mysql_data = match.groupdict()
+        self.host = mysql_data.get("host")
+        self.port = int(mysql_data.get("port"))
+        self.username = mysql_data.get("username")
+        self.password = mysql_data.get("password")
+        self.database = mysql_data.get("database")
+        self.uri = mysql_uri
+
+
+def validate_config(config: ConfigData):
+    """Validate charm configuration.
+
+    Args:
+        config (ConfigData): Charm configuration.
+
+    Raises:
+        config_validator.ValidationError if the validation failed.
+    """
+    kwargs: Dict[str, Any] = config
+    ConfigModel(**kwargs)
+    ConfigLdapModel(**kwargs)
+
+
+def get_environment(
+    service_name: str, config: ConfigData, mysql_data: MysqlConnectionData
+) -> Dict[str, Any]:
+    """Get environment variables.
+
+    Args:
+        service_name (str): Cluster IP service name.
+        config (ConfigData): Charm configuration.
+
+    Returns:
+        Dict[str, Any]: Dictionary with the environment variables for Keystone service.
+    """
+    kwargs: Dict[str, Any] = config
+    config = ConfigModel(**kwargs)
+    config_ldap = ConfigLdapModel(**kwargs)
+    environment = {
+        "DB_HOST": mysql_data.host,
+        "DB_PORT": mysql_data.port,
+        "ROOT_DB_USER": mysql_data.username,
+        "ROOT_DB_PASSWORD": mysql_data.password,
+        "REGION_ID": config.region_id,
+        "KEYSTONE_HOST": service_name,
+        "KEYSTONE_DB_PASSWORD": config.keystone_db_password,
+        "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_ldap.ldap_enabled:
+        environment.update(
+            {
+                "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
+                "LDAP_URL": config_ldap.ldap_url,
+                "LDAP_PAGE_SIZE": str(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": str(config_ldap.ldap_user_enabled_mask),
+                "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default,
+                "LDAP_USER_ENABLED_INVERT": str(config_ldap.ldap_user_enabled_invert),
+                "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass,
+            }
+        )
+        if config_ldap.ldap_use_starttls:
+            environment.update(
+                {
+                    "LDAP_USE_STARTTLS": str(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,
+                }
+            )
+        optional_ldap_configs = {
+            "LDAP_BIND_USER": config_ldap.ldap_bind_user,
+            "LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password,
+            "LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn,
+            "LDAP_USER_FILTER": config_ldap.ldap_user_filter,
+            "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute,
+            "LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals,
+            "LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn,
+            "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
+        }
+        for env, value in optional_ldap_configs.items():
+            if value:
+                environment[env] = value
+    return environment
+
+
+class ConfigModel(ConfigValidator):
+    """Keystone Configuration."""
+
+    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
+    mysql_uri: Optional[str]
+
+
+class ConfigLdapModel(ConfigValidator):
+    """LDAP Configuration."""
+
+    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[str]
+    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]
diff --git a/installers/charm/osm-keystone/src/interfaces.py b/installers/charm/osm-keystone/src/interfaces.py
new file mode 100644
index 0000000..7b019dd
--- /dev/null
+++ b/installers/charm/osm-keystone/src/interfaces.py
@@ -0,0 +1,190 @@
+# 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
+#
+#
+# This file populates the Actions tab on Charmhub.
+# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
+
+"""Interfaces used by this charm."""
+
+import ops.charm
+import ops.framework
+import ops.model
+
+
+class BaseRelationClient(ops.framework.Object):
+    """Requires side of a Kafka Endpoint."""
+
+    def __init__(
+        self,
+        charm: ops.charm.CharmBase,
+        relation_name: str,
+        mandatory_fields: list = [],
+    ):
+        super().__init__(charm, relation_name)
+        self.relation_name = relation_name
+        self.mandatory_fields = mandatory_fields
+        self._update_relation()
+
+    def get_data_from_unit(self, key: str):
+        """Get data from unit relation data."""
+        if not self.relation:
+            # This update relation doesn't seem to be needed, but I added it because apparently
+            # the data is empty in the unit tests.
+            # In reality, the constructor is called in every hook.
+            # In the unit tests when doing an update_relation_data, apparently it is not called.
+            self._update_relation()
+        if self.relation:
+            for unit in self.relation.units:
+                data = self.relation.data[unit].get(key)
+                if data:
+                    return data
+
+    def get_data_from_app(self, key: str):
+        """Get data from app relation data."""
+        if not self.relation or self.relation.app not in self.relation.data:
+            # This update relation doesn't seem to be needed, but I added it because apparently
+            # the data is empty in the unit tests.
+            # In reality, the constructor is called in every hook.
+            # In the unit tests when doing an update_relation_data, apparently it is not called.
+            self._update_relation()
+        if self.relation and self.relation.app in self.relation.data:
+            data = self.relation.data[self.relation.app].get(key)
+            if data:
+                return data
+
+    def is_missing_data_in_unit(self):
+        """Check if mandatory fields are present in any of the unit's relation data."""
+        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
+
+    def is_missing_data_in_app(self):
+        """Check if mandatory fields are set in relation data."""
+        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
+
+    def _update_relation(self):
+        self.relation = self.framework.model.get_relation(self.relation_name)
+
+
+class MysqlClient(BaseRelationClient):
+    """Requires side of a Mysql Endpoint."""
+
+    mandatory_fields = ["host", "port", "user", "password", "root_password"]
+
+    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
+        super().__init__(charm, relation_name, self.mandatory_fields)
+
+    @property
+    def host(self):
+        """Host."""
+        return self.get_data_from_unit("host")
+
+    @property
+    def port(self):
+        """Port."""
+        return self.get_data_from_unit("port")
+
+    @property
+    def user(self):
+        """User."""
+        return self.get_data_from_unit("user")
+
+    @property
+    def password(self):
+        """Password."""
+        return self.get_data_from_unit("password")
+
+    @property
+    def root_password(self):
+        """Root password."""
+        return self.get_data_from_unit("root_password")
+
+    @property
+    def database(self):
+        """Database."""
+        return self.get_data_from_unit("database")
+
+    def get_root_uri(self, database: str):
+        """Get the URI for the mysql connection with the root user credentials.
+
+        Args:
+            database: Database name
+
+        Return:
+            A string with the following format:
+                mysql://root:<root_password>@<mysql_host>:<mysql_port>/<database>
+        """
+        return "mysql://root:{}@{}:{}/{}".format(
+            self.root_password, self.host, self.port, database
+        )
+
+    def get_uri(self):
+        """Get the URI for the mysql connection with the standard user credentials.
+
+        Args:
+            database: Database name
+        Return:
+            A string with the following format:
+                    mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
+        """
+        return "mysql://{}:{}@{}:{}/{}".format(
+            self.user, self.password, self.host, self.port, self.database
+        )
+
+
+class KeystoneServer(ops.framework.Object):
+    """Provides side of a Keystone Endpoint."""
+
+    relation_name: str = None
+
+    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
+        super().__init__(charm, relation_name)
+        self.relation_name = relation_name
+
+    def publish_info(
+        self,
+        host: str,
+        port: int,
+        user_domain_name: str,
+        project_domain_name: str,
+        username: str,
+        password: str,
+        service: str,
+        keystone_db_password: str,
+        region_id: str,
+        admin_username: str,
+        admin_password: str,
+        admin_project_name: str,
+    ):
+        """Publish information in Keystone relation."""
+        if self.framework.model.unit.is_leader():
+            for relation in self.framework.model.relations[self.relation_name]:
+                relation_data = relation.data[self.framework.model.app]
+                relation_data["host"] = str(host)
+                relation_data["port"] = str(port)
+                relation_data["user_domain_name"] = str(user_domain_name)
+                relation_data["project_domain_name"] = str(project_domain_name)
+                relation_data["username"] = str(username)
+                relation_data["password"] = str(password)
+                relation_data["service"] = str(service)
+                relation_data["keystone_db_password"] = str(keystone_db_password)
+                relation_data["region_id"] = str(region_id)
+                relation_data["admin_username"] = str(admin_username)
+                relation_data["admin_password"] = str(admin_password)
+                relation_data["admin_project_name"] = str(admin_project_name)