diff --git a/charms/aodh-k8s/.gitignore b/.gitignore
similarity index 84%
rename from charms/aodh-k8s/.gitignore
rename to .gitignore
index 24ff2e41..32543ded 100644
--- a/charms/aodh-k8s/.gitignore
+++ b/.gitignore
@@ -1,11 +1,11 @@
 venv/
 build/
+.idea/
+.vscode/
 *.charm
 .tox/
 .coverage
 __pycache__/
 *.py[cod]
-.idea
-.vscode/
-*.swp
+**.swp
 .stestr/
diff --git a/charms/heat-k8s/.gitreview b/.gitreview
similarity index 63%
rename from charms/heat-k8s/.gitreview
rename to .gitreview
index 5afeb9d1..acf6b77a 100644
--- a/charms/heat-k8s/.gitreview
+++ b/.gitreview
@@ -1,5 +1,5 @@
 [gerrit]
 host=review.opendev.org
 port=29418
-project=openstack/charm-heat-k8s.git
+project=openstack/sunbeam-charms.git
 defaultbranch=main
diff --git a/charms/keystone-k8s/.jujuignore b/.jujuignore
similarity index 100%
rename from charms/keystone-k8s/.jujuignore
rename to .jujuignore
diff --git a/charms/aodh-k8s/.stestr.conf b/.stestr.conf
similarity index 100%
rename from charms/aodh-k8s/.stestr.conf
rename to .stestr.conf
diff --git a/charms/aodh-k8s/.gitreview b/charms/aodh-k8s/.gitreview
deleted file mode 100644
index c7cbdfc4..00000000
--- a/charms/aodh-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-aodh-k8s.git
-defaultbranch=main
diff --git a/charms/aodh-k8s/.zuul.yaml b/charms/aodh-k8s/.zuul.yaml
deleted file mode 100644
index 87b6ace0..00000000
--- a/charms/aodh-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: aodh-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/aodh-k8s/charmcraft.yaml b/charms/aodh-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/aodh-k8s/charmcraft.yaml
+++ b/charms/aodh-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/aodh-k8s/fetch-libs.sh b/charms/aodh-k8s/fetch-libs.sh
deleted file mode 100755
index e7772471..00000000
--- a/charms/aodh-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644
index 4cf26164..00000000
--- a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
+++ /dev/null
@@ -1,408 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# Licensed under the Apache2.0, see LICENCE file in charm source for details.
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-- "self" (the charm itself)
-- config_dict
-
-`config_dict` accepts the following keys:
-- additional-hostnames
-- backend-protocol
-- limit-rps
-- limit-whitelist
-- max-body-size
-- owasp-modsecurity-crs
-- owasp-modsecurity-custom-rules
-- path-routes
-- retry-errors
-- rewrite-enabled
-- rewrite-target
-- service-hostname (required)
-- service-name (required)
-- service-namespace
-- service-port (required)
-- session-cookie-max-age
-- tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {
-        "service-hostname": self.config["external_hostname"],
-        "service-name": self.app.name,
-        "service-port": 80,
-    }
-)
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler, for the relation
-changed event to be properly handled.
-"""
-
-import copy
-import logging
-from typing import Dict
-
-from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-INGRESS_RELATION_NAME = "ingress"
-INGRESS_PROXY_RELATION_NAME = "ingress-proxy"
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 16
-
-LOGGER = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "backend-protocol",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "owasp-modsecurity-custom-rules",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-RELATION_INTERFACES_MAPPINGS = {
-    "service-hostname": "host",
-    "service-name": "name",
-    "service-namespace": "model",
-    "service-port": "port",
-}
-RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values())
-
-
-class IngressAvailableEvent(EventBase):
-    """IngressAvailableEvent custom event.
-
-    This event indicates the Ingress provider is available.
-    """
-
-
-class IngressProxyAvailableEvent(EventBase):
-    """IngressProxyAvailableEvent custom event.
-
-    This event indicates the IngressProxy provider is available.
-    """
-
-
-class IngressBrokenEvent(RelationBrokenEvent):
-    """IngressBrokenEvent custom event.
-
-    This event indicates the Ingress provider is broken.
-    """
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events.
-
-    Attrs:
-        ingress_available: Event to indicate that Ingress is available.
-        ingress_proxy_available: Event to indicate that IngressProxy is available.
-        ingress_broken: Event to indicate that Ingress is broken.
-    """
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_proxy_available = EventSource(IngressProxyAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-
-    Attrs:
-        model: Juju model where the charm is deployed.
-        config_dict: Contains all the configuration options for Ingress.
-    """
-
-    def __init__(self, charm: CharmBase, config_dict: Dict) -> None:
-        """Init function for the IngressRequires class.
-
-        Args:
-            charm: The charm that requires the ingress relation.
-            config_dict: Contains all the configuration options for Ingress.
-        """
-        super().__init__(charm, INGRESS_RELATION_NAME)
-
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
-
-        # Set default values.
-        default_relation_fields = {
-            "service-namespace": self.model.name,
-        }
-        config_dict.update(
-            (key, value)
-            for key, value in default_relation_fields.items()
-            if key not in config_dict or not config_dict[key]
-        )
-
-        self.config_dict = self._convert_to_relation_interface(config_dict)
-
-    @staticmethod
-    def _convert_to_relation_interface(config_dict: Dict) -> Dict:
-        """Create a new relation dict that conforms with charm-relation-interfaces.
-
-        Args:
-            config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces.
-
-        Returns:
-            The Ingress configuration conforming with charm-relation-interfaces.
-        """
-        config_dict = copy.copy(config_dict)
-        config_dict.update(
-            (key, config_dict[old_key])
-            for old_key, key in RELATION_INTERFACES_MAPPINGS.items()
-            if old_key in config_dict and config_dict[old_key]
-        )
-        return config_dict
-
-    def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool:
-        """Check our config dict for errors.
-
-        Args:
-            config_dict: Contains all the configuration options for Ingress.
-            update_only: If the charm needs to update only existing keys.
-
-        Returns:
-            If we need to update the config dict or not.
-        """
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            config_key
-            for config_key in config_dict
-            if config_key
-            not in REQUIRED_INGRESS_RELATION_FIELDS
-            | OPTIONAL_INGRESS_RELATION_FIELDS
-            | RELATION_INTERFACES_MAPPINGS_VALUES
-        ]
-        if unknown:
-            LOGGER.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = tuple(
-                config_key
-                for config_key in REQUIRED_INGRESS_RELATION_FIELDS
-                if config_key not in self.config_dict
-            )
-            if missing:
-                LOGGER.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handle the relation-changed event.
-
-        Args:
-            event: Event triggering the relation-changed hook for the relation.
-        """
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors(config_dict=self.config_dict):
-                return
-            event.relation.data[self.model.app].update(
-                (key, str(self.config_dict[key])) for key in self.config_dict
-            )
-
-    def update_config(self, config_dict: Dict) -> None:
-        """Allow for updates to relation.
-
-        Args:
-            config_dict: Contains all the configuration options for Ingress.
-
-        Attrs:
-            config_dict: Contains all the configuration options for Ingress.
-        """
-        if self.model.unit.is_leader():
-            self.config_dict = self._convert_to_relation_interface(config_dict)
-            if self._config_dict_errors(self.config_dict, update_only=True):
-                return
-            relation = self.model.get_relation(INGRESS_RELATION_NAME)
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressBaseProvides(Object):
-    """Parent class for IngressProvides and IngressProxyProvides.
-
-    Attrs:
-        model: Juju model where the charm is deployed.
-    """
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        """Init function for the IngressProxyProvides class.
-
-        Args:
-            charm: The charm that provides the ingress-proxy relation.
-        """
-        super().__init__(charm, relation_name)
-        self.charm = charm
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handle a change to the ingress/ingress-proxy relation.
-
-        Confirm we have the fields we expect to receive.
-
-        Args:
-            event: Event triggering the relation-changed hook for the relation.
-        """
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        relation_name = event.relation.name
-
-        assert event.app is not None  # nosec
-        if not event.relation.data[event.app]:
-            LOGGER.info(
-                "%s hasn't finished configuring, waiting until relation is changed again.",
-                relation_name,
-            )
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None
-        )
-
-        if missing_fields:
-            LOGGER.warning(
-                "Missing required data fields for %s relation: %s",
-                relation_name,
-                ", ".join(missing_fields),
-            )
-            self.model.unit.status = BlockedStatus(
-                f"Missing fields for {relation_name}: {', '.join(missing_fields)}"
-            )
-
-        if relation_name == INGRESS_RELATION_NAME:
-            # Conform to charm-relation-interfaces.
-            if "name" in ingress_data and "port" in ingress_data:
-                name = ingress_data["name"]
-                port = ingress_data["port"]
-            else:
-                name = ingress_data["service-name"]
-                port = ingress_data["service-port"]
-            event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/"
-
-            # Create an event that our charm can use to decide it's okay to
-            # configure the ingress.
-            self.charm.on.ingress_available.emit()
-        elif relation_name == INGRESS_PROXY_RELATION_NAME:
-            self.charm.on.ingress_proxy_available.emit()
-
-
-class IngressProvides(IngressBaseProvides):
-    """Class containing the functionality for the 'provides' side of the 'ingress' relation.
-
-    Attrs:
-        charm: The charm that provides the ingress relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm: CharmBase) -> None:
-        """Init function for the IngressProvides class.
-
-        Args:
-            charm: The charm that provides the ingress relation.
-        """
-        super().__init__(charm, INGRESS_RELATION_NAME)
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken
-        )
-
-    def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
-        """Handle a relation-broken event in the ingress relation.
-
-        Args:
-            event: Event triggering the relation-broken hook for the relation.
-        """
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit(event.relation)
-
-
-class IngressProxyProvides(IngressBaseProvides):
-    """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation.
-
-    Attrs:
-        charm: The charm that provides the ingress-proxy relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm: CharmBase) -> None:
-        """Init function for the IngressProxyProvides class.
-
-        Args:
-            charm: The charm that provides the ingress-proxy relation.
-        """
-        super().__init__(charm, INGRESS_PROXY_RELATION_NAME)
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(
-            charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
diff --git a/charms/aodh-k8s/osci.yaml b/charms/aodh-k8s/osci.yaml
deleted file mode 100644
index b59f7058..00000000
--- a/charms/aodh-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: aodh-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/aodh-k8s/rename.sh b/charms/aodh-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/aodh-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/aodh-k8s/requirements.txt b/charms/aodh-k8s/requirements.txt
index 5806e426..1dcec118 100644
--- a/charms/aodh-k8s/requirements.txt
+++ b/charms/aodh-k8s/requirements.txt
@@ -1,9 +1,11 @@
 ops
 jinja2
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 pydantic<2.0
 
 # Uncomment below if charm relates to ceph
 # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
 # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/aodh-k8s/src/templates/parts/section-service-user b/charms/aodh-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 165fbe71..00000000
--- a/charms/aodh-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/aodh-k8s/test-requirements.txt b/charms/aodh-k8s/test-requirements.txt
deleted file mode 100644
index 276e5bee..00000000
--- a/charms/aodh-k8s/test-requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/aodh-k8s/tests/unit/test_charm.py b/charms/aodh-k8s/tests/unit/test_charm.py
index a5967b30..0329067b 100644
--- a/charms/aodh-k8s/tests/unit/test_charm.py
+++ b/charms/aodh-k8s/tests/unit/test_charm.py
@@ -16,9 +16,8 @@
 
 """Tests for gnocchi charm."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _AodhOperatorCharm(charm.AodhOperatorCharm):
diff --git a/charms/aodh-k8s/tox.ini b/charms/aodh-k8s/tox.ini
deleted file mode 100644
index 067963f9..00000000
--- a/charms/aodh-k8s/tox.ini
+++ /dev/null
@@ -1,165 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/barbican-k8s/.flake8 b/charms/barbican-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/barbican-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/barbican-k8s/.gitignore b/charms/barbican-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/barbican-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/barbican-k8s/.gitreview b/charms/barbican-k8s/.gitreview
deleted file mode 100644
index dbfd1ea0..00000000
--- a/charms/barbican-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-barbican-k8s.git
-defaultbranch=main
diff --git a/charms/barbican-k8s/.jujuignore b/charms/barbican-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/barbican-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/barbican-k8s/.stestr.conf b/charms/barbican-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/barbican-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/barbican-k8s/.zuul.yaml b/charms/barbican-k8s/.zuul.yaml
deleted file mode 100644
index c7711b8f..00000000
--- a/charms/barbican-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: barbican-k8s
-      juju_channel: 3.2/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/barbican-k8s/charmcraft.yaml b/charms/barbican-k8s/charmcraft.yaml
index 58193c73..de20145a 100644
--- a/charms/barbican-k8s/charmcraft.yaml
+++ b/charms/barbican-k8s/charmcraft.yaml
@@ -20,4 +20,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/barbican-k8s/fetch-libs.sh b/charms/barbican-k8s/fetch-libs.sh
deleted file mode 100755
index a573dd64..00000000
--- a/charms/barbican-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
deleted file mode 100644
index 154fab83..00000000
--- a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
+++ /dev/null
@@ -1,392 +0,0 @@
-"""IdentityResourceProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the identity_ops interface.
-
-Import `IdentityResourceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_ops"
-
-Also provide additional parameters to the charm object:
-    - request
-
-Three events are also available to respond to:
-    - provider_ready
-    - provider_goneaway
-    - response_avaialable
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
-
-class IdentityResourceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityResource Requires
-        self.identity_resource = IdentityResourceRequires(
-            self, "identity_ops",
-        )
-        self.framework.observe(
-            self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
-        self.framework.observe(
-            self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
-        self.framework.observe(
-            self.identity_resource.on.response_available, self._on_identity_resource_response)
-
-    def _on_identity_resource_ready(self, event):
-        '''React to the IdentityResource provider_ready event.
-
-        This event happens when n IdentityResource relation is added to the
-        model. Ready to send any ops to keystone.
-        '''
-        # Ready to send any ops.
-        pass
-
-    def _on_identity_resource_response(self, event):
-        '''React to the IdentityResource response_available event.
-
-        The IdentityResource interface will provide the response for the ops sent.
-        '''
-        # Read the response for the ops sent.
-        pass
-
-    def _on_identity_resource_goneaway(self, event):
-        '''React to the IdentityResource goneaway event.
-
-        This event happens when an IdentityResource relation is removed.
-        '''
-        # IdentityResource Relation has goneaway. No ops can be sent.
-        pass
-```
-
-A sample ops request can be of format
-{
-    "id": <request id>
-    "tag": <string to identify request>
-    "ops": [
-        {
-            "name": <op name>,
-            "params": {
-                <param 1>: <value 1>,
-                <param 2>: <value 2>
-            }
-        }
-    ]
-}
-
-For any sensitive data in the ops params, the charm can create secrets and pass
-secret id instead of sensitive data as part of ops request. The charm should
-ensure to grant secret access to provider charm i.e., keystone over relation.
-The secret content should hold the sensitive data with same name as param name.
-"""
-
-import json
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import (
-    EventBase,
-    EventSource,
-    Object,
-    ObjectEvents,
-    StoredState,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b419d4d8249e423487daafc3665ed06f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-REQUEST_NOT_SENT = 1
-REQUEST_SENT = 2
-REQUEST_PROCESSED = 3
-
-
-class IdentityOpsProviderReadyEvent(RelationEvent):
-    """Has IdentityOpsProviderReady Event."""
-
-    pass
-
-
-class IdentityOpsResponseEvent(RelationEvent):
-    """Has IdentityOpsResponse Event."""
-
-    pass
-
-
-class IdentityOpsProviderGoneAwayEvent(RelationEvent):
-    """Has IdentityOpsProviderGoneAway Event."""
-
-    pass
-
-
-class IdentityResourceResponseEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    provider_ready = EventSource(IdentityOpsProviderReadyEvent)
-    response_available = EventSource(IdentityOpsResponseEvent)
-    provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
-
-
-class IdentityResourceRequires(Object):
-    """IdentityResourceRequires class."""
-
-    on = IdentityResourceResponseEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self._stored.set_default(provider_ready=False, requests=[])
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_resource_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_resource_relation_broken,
-        )
-
-    def _on_identity_resource_relation_joined(
-        self, event: RelationJoinedEvent
-    ):
-        """Handle IdentityResource joined."""
-        self._stored.provider_ready = True
-        self.on.provider_ready.emit(event.relation)
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        id_ = self.response.get("id")
-        self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
-        self.on.response_available.emit(event.relation)
-
-    def _on_identity_resource_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle IdentityResource broken."""
-        self._stored.provider_ready = False
-        self.on.provider_goneaway.emit(event.relation)
-
-    @property
-    def _identity_resource_rel(self) -> Optional[Relation]:
-        """The IdentityResource relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def response(self) -> dict:
-        """Response object from keystone."""
-        response = self.get_remote_app_data("response")
-        if not response:
-            return {}
-
-        try:
-            return json.loads(response)
-        except Exception as e:
-            logger.debug(str(e))
-
-        return {}
-
-    def save_request_in_store(
-        self, id: str, tag: str, ops: list, state: int
-    ) -> None:
-        """Save request in the store."""
-        if id is None:
-            return
-
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                if tag:
-                    request["tag"] = tag
-                if ops:
-                    request["ops"] = ops
-                request["state"] = state
-                return
-
-        # New request
-        self._stored.requests.append(
-            {"id": id, "tag": tag, "ops": ops, "state": state}
-        )
-
-    def get_request_from_store(self, id: str) -> dict:
-        """Get request from the stote."""
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                return request
-
-        return {}
-
-    def is_request_processed(self, id: str) -> bool:
-        """Check if request is processed."""
-        for request in self._stored.requests:
-            if (
-                request.get("id") == id
-                and request.get("state") == REQUEST_PROCESSED
-            ):
-                return True
-
-        return False
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._identity_resource_rel:
-            data = self._identity_resource_rel.data[
-                self._identity_resource_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    def ready(self) -> bool:
-        """Interface is ready or not.
-
-        Interface is considered ready if the op request is processed
-        and response is sent. In case of non leader unit, just consider
-        the interface is ready.
-        """
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, set the interface to ready")
-            return True
-
-        try:
-            app_data = self._identity_resource_rel.data[self.charm.app]
-            if "request" not in app_data:
-                return False
-
-            request = json.loads(app_data["request"])
-            request_id = request.get("id")
-            response_id = self.response.get("id")
-            if request_id == response_id:
-                return True
-        except Exception as e:
-            logger.debug(str(e))
-
-        return False
-
-    def request_ops(self, request: dict) -> None:
-        """Request keystone ops."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending request")
-            return
-
-        id_ = request.get("id")
-        tag = request.get("tag")
-        ops = request.get("ops")
-        req = self.get_request_from_store(id_)
-        if req and req.get("state") == REQUEST_PROCESSED:
-            logger.debug("Request {id_} already processed")
-            return
-
-        if not self._stored.provider_ready:
-            self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
-            logger.debug("Keystone not yet ready to take requests")
-            return
-
-        logger.debug("Requesting ops to keystone")
-        app_data = self._identity_resource_rel.data[self.charm.app]
-        app_data["request"] = json.dumps(request)
-        self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
-
-
-class IdentityOpsRequestEvent(EventBase):
-    """Has IdentityOpsRequest Event."""
-
-    def __init__(self, handle, relation_id, relation_name, request):
-        """Initialise event."""
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.request = request
-
-    def snapshot(self):
-        """Snapshot the event."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "request": self.request,
-        }
-
-    def restore(self, snapshot):
-        """Restore the event."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.request = snapshot["request"]
-
-
-class IdentityResourceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    process_op = EventSource(IdentityOpsRequestEvent)
-
-
-class IdentityResourceProvides(Object):
-    """IdentityResourceProvides class."""
-
-    on = IdentityResourceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        request = event.relation.data[event.relation.app].get("request", {})
-        self.on.process_op.emit(
-            event.relation.id, event.relation.name, request
-        )
-
-    def set_ops_response(
-        self, relation_id: str, relation_name: str, ops_response: dict
-    ) -> None:
-        """Set response to ops request."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending response")
-            return
-
-        logger.debug("Update response from keystone")
-        _identity_resource_rel = self.charm.model.get_relation(
-            relation_name, relation_id
-        )
-        if not _identity_resource_rel:
-            # Relation has disappeared so skip send of data
-            return
-
-        app_data = _identity_resource_rel.data[self.charm.app]
-        app_data["response"] = json.dumps(ops_response)
diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/barbican-k8s/osci.yaml b/charms/barbican-k8s/osci.yaml
deleted file mode 100644
index 7e1239dd..00000000
--- a/charms/barbican-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: barbican-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/barbican-k8s/pyproject.toml b/charms/barbican-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/barbican-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/barbican-k8s/rename.sh b/charms/barbican-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/barbican-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/barbican-k8s/requirements.txt b/charms/barbican-k8s/requirements.txt
index 0f23be49..b91c068e 100644
--- a/charms/barbican-k8s/requirements.txt
+++ b/charms/barbican-k8s/requirements.txt
@@ -13,4 +13,6 @@ lightkube-models
 ops
 pwgen
 pytest-interface-tester
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/barbican-k8s/src/templates/barbican.conf b/charms/barbican-k8s/src/templates/barbican.conf
index 0253b41d..bd76ac38 100644
--- a/charms/barbican-k8s/src/templates/barbican.conf
+++ b/charms/barbican-k8s/src/templates/barbican.conf
@@ -10,6 +10,8 @@ sql_connection = {{ database.connection }}
 db_auto_create = false
 
 {% include "parts/section-identity" %}
+# XXX Region should come from the id relation here
+region_name = {{ options.region }}
 
 {% include "parts/section-service-user" %}
 
diff --git a/charms/barbican-k8s/src/templates/parts/section-identity b/charms/barbican-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 92dabb09..00000000
--- a/charms/barbican-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,27 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
-
-# XXX Region should come from the id relation here
-region_name = {{ options.region }}
diff --git a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/barbican-k8s/test-requirements.txt b/charms/barbican-k8s/test-requirements.txt
deleted file mode 100644
index 0b8ca0cd..00000000
--- a/charms/barbican-k8s/test-requirements.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
diff --git a/charms/barbican-k8s/tests/unit/test_barbican_charm.py b/charms/barbican-k8s/tests/unit/test_barbican_charm.py
index eff64acc..6dcaaa76 100644
--- a/charms/barbican-k8s/tests/unit/test_barbican_charm.py
+++ b/charms/barbican-k8s/tests/unit/test_barbican_charm.py
@@ -16,9 +16,8 @@
 
 """Unit tests for Barbican operator."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _BarbicanTestOperatorCharm(charm.BarbicanOperatorCharm):
diff --git a/charms/barbican-k8s/tox.ini b/charms/barbican-k8s/tox.ini
deleted file mode 100644
index fbaa02c5..00000000
--- a/charms/barbican-k8s/tox.ini
+++ /dev/null
@@ -1,169 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/ceilometer-k8s/.gitignore b/charms/ceilometer-k8s/.gitignore
deleted file mode 100644
index 24ff2e41..00000000
--- a/charms/ceilometer-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
diff --git a/charms/ceilometer-k8s/.gitreview b/charms/ceilometer-k8s/.gitreview
deleted file mode 100644
index 1b4bcda6..00000000
--- a/charms/ceilometer-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-ceilometer-k8s.git
-defaultbranch=main
diff --git a/charms/ceilometer-k8s/.stestr.conf b/charms/ceilometer-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/ceilometer-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/ceilometer-k8s/.zuul.yaml b/charms/ceilometer-k8s/.zuul.yaml
deleted file mode 100644
index 18a4323b..00000000
--- a/charms/ceilometer-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: ceilometer-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/ceilometer-k8s/charmcraft.yaml b/charms/ceilometer-k8s/charmcraft.yaml
index ac49568b..98bf024f 100644
--- a/charms/ceilometer-k8s/charmcraft.yaml
+++ b/charms/ceilometer-k8s/charmcraft.yaml
@@ -27,4 +27,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/ceilometer-k8s/fetch-libs.sh b/charms/ceilometer-k8s/fetch-libs.sh
deleted file mode 100755
index 16cf2cad..00000000
--- a/charms/ceilometer-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.gnocchi_k8s.v0.gnocchi_service
-# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-# charmcraft fetch-lib charms.traefik_k8s.v1.ingress
diff --git a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/ceilometer-k8s/osci.yaml b/charms/ceilometer-k8s/osci.yaml
deleted file mode 100644
index 05db36a3..00000000
--- a/charms/ceilometer-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: ceilometer-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/ceilometer-k8s/pyproject.toml b/charms/ceilometer-k8s/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/ceilometer-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/ceilometer-k8s/rename.sh b/charms/ceilometer-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/ceilometer-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/ceilometer-k8s/requirements.txt b/charms/ceilometer-k8s/requirements.txt
index 20a477f7..9e7a45a8 100644
--- a/charms/ceilometer-k8s/requirements.txt
+++ b/charms/ceilometer-k8s/requirements.txt
@@ -1,8 +1,10 @@
 ops
 jinja2
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 
 # Uncomment below if charm relates to ceph
 # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
 # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/ceilometer-k8s/src/templates/ceilometer.conf b/charms/ceilometer-k8s/src/templates/ceilometer.conf
index f976d370..1a1b265e 100644
--- a/charms/ceilometer-k8s/src/templates/ceilometer.conf
+++ b/charms/ceilometer-k8s/src/templates/ceilometer.conf
@@ -20,8 +20,8 @@ archive_policy = low
 [keystone_authtoken]
 {% include "parts/identity-data-id-creds" %}
 
-{% include "parts/section-service-user-id-creds" %}
+{% include "parts/section-service-user-from-identity-credentials" %}
 
-{% include "parts/section-service-credentials" %}
+{% include "parts/section-service-credentials-from-identity-service" %}
 
 {% include "parts/section-oslo-messaging-rabbit" %}
diff --git a/charms/ceilometer-k8s/src/templates/parts/database-connection b/charms/ceilometer-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 1fd70ce2..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data b/charms/ceilometer-k8s/src/templates/parts/identity-data
deleted file mode 100644
index 706d9d13..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/identity-data
+++ /dev/null
@@ -1,23 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-database b/charms/ceilometer-k8s/src/templates/parts/section-database
deleted file mode 100644
index 986d9b10..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,3 +0,0 @@
-[database]
-{% include "parts/database-connection" %}
-connection_recycle_time = 200
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-federation b/charms/ceilometer-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-identity b/charms/ceilometer-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 7568a9a4..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,2 +0,0 @@
-[keystone_authtoken]
-{% include "parts/identity-data" %}
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-middleware b/charms/ceilometer-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user b/charms/ceilometer-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 165fbe71..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-signing b/charms/ceilometer-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/ceilometer-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/ceilometer-k8s/test-requirements.txt b/charms/ceilometer-k8s/test-requirements.txt
deleted file mode 100644
index 276e5bee..00000000
--- a/charms/ceilometer-k8s/test-requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/ceilometer-k8s/tests/unit/test_charm.py b/charms/ceilometer-k8s/tests/unit/test_charm.py
index ff3e7847..4a287b2d 100644
--- a/charms/ceilometer-k8s/tests/unit/test_charm.py
+++ b/charms/ceilometer-k8s/tests/unit/test_charm.py
@@ -16,9 +16,8 @@
 
 """Tests for gnocchi charm."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm):
diff --git a/charms/ceilometer-k8s/tox.ini b/charms/ceilometer-k8s/tox.ini
deleted file mode 100644
index 067963f9..00000000
--- a/charms/ceilometer-k8s/tox.ini
+++ /dev/null
@@ -1,165 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/cinder-ceph-k8s/.flake8 b/charms/cinder-ceph-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/cinder-ceph-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/cinder-ceph-k8s/.gitignore b/charms/cinder-ceph-k8s/.gitignore
deleted file mode 100644
index ba40a601..00000000
--- a/charms/cinder-ceph-k8s/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-venv/
-build/
-*.charm
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr
diff --git a/charms/cinder-ceph-k8s/.gitreview b/charms/cinder-ceph-k8s/.gitreview
deleted file mode 100644
index 3b9d90fa..00000000
--- a/charms/cinder-ceph-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-cinder-ceph-k8s.git
-defaultbranch=main
diff --git a/charms/cinder-ceph-k8s/.jujuignore b/charms/cinder-ceph-k8s/.jujuignore
deleted file mode 100644
index 56ce099b..00000000
--- a/charms/cinder-ceph-k8s/.jujuignore
+++ /dev/null
@@ -1,5 +0,0 @@
-/venv
-*.py[cod]
-*.charm
-.tox
-.stestr
diff --git a/charms/cinder-ceph-k8s/.stestr.conf b/charms/cinder-ceph-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/cinder-ceph-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/cinder-ceph-k8s/.zuul.yaml b/charms/cinder-ceph-k8s/.zuul.yaml
deleted file mode 100644
index 3176ef44..00000000
--- a/charms/cinder-ceph-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: cinder-ceph-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/cinder-ceph-k8s/charmcraft.yaml b/charms/cinder-ceph-k8s/charmcraft.yaml
index 0c6b3bdb..cbd210b0 100644
--- a/charms/cinder-ceph-k8s/charmcraft.yaml
+++ b/charms/cinder-ceph-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/cinder-ceph-k8s/fetch-libs.sh b/charms/cinder-ceph-k8s/fetch-libs.sh
deleted file mode 100755
index defa6987..00000000
--- a/charms/cinder-ceph-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.cinder_k8s.v0.storage_backend
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 35556622..00000000
--- a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,518 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 0
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
diff --git a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/cinder-ceph-k8s/osci.yaml b/charms/cinder-ceph-k8s/osci.yaml
deleted file mode 100644
index 2d77a6de..00000000
--- a/charms/cinder-ceph-k8s/osci.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-- project:
-    templates:
-      - charm-unit-jobs-py38
-      - charm-unit-jobs-py310
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: cinder-ceph-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/cinder-ceph-k8s/pyproject.toml b/charms/cinder-ceph-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/cinder-ceph-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/cinder-ceph-k8s/rename.sh b/charms/cinder-ceph-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/cinder-ceph-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/cinder-ceph-k8s/requirements.txt b/charms/cinder-ceph-k8s/requirements.txt
index e3e15ef9..6a1de2d9 100644
--- a/charms/cinder-ceph-k8s/requirements.txt
+++ b/charms/cinder-ceph-k8s/requirements.txt
@@ -11,7 +11,6 @@ lightkube
 lightkube-models
 requests # Drop - not needed in storage backend interface.
 ops
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 
 git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
 
@@ -23,3 +22,6 @@ git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
 # TODO
 requests # Drop - not needed in storage backend interface.
 netifaces # Drop when charmhelpers dependency is removed.
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/cinder-ceph-k8s/test-requirements.txt b/charms/cinder-ceph-k8s/test-requirements.txt
deleted file mode 100644
index a9b0d698..00000000
--- a/charms/cinder-ceph-k8s/test-requirements.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py
index 0fd046cd..7eaad75c 100644
--- a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py
+++ b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py
@@ -18,16 +18,16 @@
 
 import json
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from mock import (
+    MagicMock,
     patch,
 )
 from ops.testing import (
     Harness,
 )
 
-import charm
-
 
 class _CinderCephOperatorCharm(charm.CinderCephOperatorCharm):
     """Charm wrapper for test usage."""
@@ -82,6 +82,7 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
     def setUp(self):
         """Setup fixtures ready for testing."""
         super().setUp(charm, self.PATCHES)
+        self.mock_event = MagicMock()
         self.harness = test_utils.get_harness(
             _CinderCephOperatorCharm, container_calls=self.container_calls
         )
@@ -118,7 +119,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
         test_utils.add_complete_db_relation(self.harness)
         add_complete_storage_backend_relation(self.harness)
         test_utils.set_all_pebbles_ready(self.harness)
-        self.assertTrue(self.harness.charm.relation_handlers_ready())
+        self.assertSetEqual(
+            self.harness.charm.get_mandatory_relations_not_ready(
+                self.mock_event
+            ),
+            set(),
+        )
+        # self.assertTrue(self.harness.charm.relation_handlers_ready())
 
     def test_ceph_access(self):
         """Test charm provides secret via ceph-access."""
@@ -132,7 +139,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
         )
         add_complete_storage_backend_relation(self.harness)
         test_utils.set_all_pebbles_ready(self.harness)
-        self.assertTrue(self.harness.charm.relation_handlers_ready())
+        # self.assertTrue(self.harness.charm.relation_handlers_ready())
+        self.assertSetEqual(
+            self.harness.charm.get_mandatory_relations_not_ready(
+                self.mock_event
+            ),
+            set(),
+        )
         rel_data = self.harness.get_relation_data(
             access_rel, self.harness.charm.unit.app.name
         )
diff --git a/charms/cinder-ceph-k8s/tox.ini b/charms/cinder-ceph-k8s/tox.ini
deleted file mode 100644
index a411adb3..00000000
--- a/charms/cinder-ceph-k8s/tox.ini
+++ /dev/null
@@ -1,160 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/cinder-k8s/.flake8 b/charms/cinder-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/cinder-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/cinder-k8s/.gitignore b/charms/cinder-k8s/.gitignore
deleted file mode 100644
index 844a0b93..00000000
--- a/charms/cinder-k8s/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-venv/
-build/
-.stestr/
-*.charm
-.tox
-.coverage
-__pycache__/
-*.py[cod]
diff --git a/charms/cinder-k8s/.gitreview b/charms/cinder-k8s/.gitreview
deleted file mode 100644
index e2834fe8..00000000
--- a/charms/cinder-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-cinder-k8s.git
-defaultbranch=main
diff --git a/charms/cinder-k8s/.jujuignore b/charms/cinder-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/cinder-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/cinder-k8s/.stestr.conf b/charms/cinder-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/cinder-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/cinder-k8s/.zuul.yaml b/charms/cinder-k8s/.zuul.yaml
deleted file mode 100644
index 8fdc72b1..00000000
--- a/charms/cinder-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: cinder-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/cinder-k8s/charmcraft.yaml b/charms/cinder-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/cinder-k8s/charmcraft.yaml
+++ b/charms/cinder-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/cinder-k8s/fetch-libs.sh b/charms/cinder-k8s/fetch-libs.sh
deleted file mode 100755
index e7772471..00000000
--- a/charms/cinder-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py
deleted file mode 100644
index 8b1aa804..00000000
--- a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""TODO: Add a proper docstring here.
-
-This is a placeholder docstring for this charm library. Docstrings are
-presented on Charmhub and updated whenever you push a new version of the
-library.
-
-Complete documentation about creating and documenting libraries can be found
-in the SDK docs at https://juju.is/docs/sdk/libraries.
-
-See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
-share and consume charm libraries. They serve to enhance collaboration
-between charmers. Use a charmer's libraries for classes that handle
-integration with their charm.
-
-Bear in mind that new revisions of the different major API versions (v0, v1,
-v2 etc) are maintained independently.  You can continue to update v0 and v1
-after you have pushed v3.
-
-Markdown is supported, following the CommonMark specification.
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "68536ea2f06d40078ccbedd7095e141c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import json
-import logging
-import requests
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-# TODO: add your code here! Happy coding!
-class StorageBackendConnectedEvent(EventBase):
-    """StorageBackend connected Event."""
-
-    pass
-
-
-class StorageBackendReadyEvent(EventBase):
-    """StorageBackend ready for use Event."""
-
-    pass
-
-
-class StorageBackendGoneAwayEvent(EventBase):
-    """StorageBackend relation has gone-away Event"""
-
-    pass
-
-
-class StorageBackendServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(StorageBackendConnectedEvent)
-    ready = EventSource(StorageBackendReadyEvent)
-    goneaway = EventSource(StorageBackendGoneAwayEvent)
-
-
-class StorageBackendRequires(Object):
-    """
-    StorageBackendRequires class
-    """
-
-    on = StorageBackendServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_storage_backend_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_storage_backend_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_storage_backend_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_storage_backend_relation_broken,
-        )
-
-    def _on_storage_backend_relation_joined(self, event):
-        """StorageBackend relation joined."""
-        logging.debug("StorageBackendRequires on_joined")
-        self.on.connected.emit()
-
-    def _on_storage_backend_relation_changed(self, event):
-        """StorageBackend relation changed."""
-        logging.debug("StorageBackendRequires on_changed")
-        self.on.ready.emit()
-
-    def _on_storage_backend_relation_broken(self, event):
-        """StorageBackend relation broken."""
-        logging.debug("StorageBackendRequires on_broken")
-        self.on.goneaway.emit()
-
-    def set_ready(self) -> None:
-        """Request access to the StorageBackend server."""
-        if self.model.unit.is_leader():
-            logging.debug(
-                "Signalling storage backends that core services are ready"
-            )
-            for relation in self.framework.model.relations[self.relation_name]:
-                relation.data[self.charm.app]["ready"] = 'true'
-
-
-class APIReadyEvent(EventBase):
-    """StorageBackendClients Ready Event."""
-
-    pass
-
-
-class StorageBackendClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    api_ready = EventSource(APIReadyEvent)
-
-
-class StorageBackendProvides(Object):
-    """
-    StorageBackendProvides class
-    """
-
-    on = StorageBackendClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_storage_backend_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_storage_backend_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_storage_backend_relation_broken,
-        )
-
-    def _on_storage_backend_relation_joined(self, event):
-        """Handle StorageBackend joined."""
-        logging.debug("StorageBackendProvides on_joined")
-
-    def remote_ready(self):
-        relation = self.framework.model.get_relation(self.relation_name)
-        if relation:
-            ready = relation.data[relation.app].get("ready")
-            return ready and json.loads(ready)
-        return False
-
-    def _on_storage_backend_relation_changed(self, event):
-        """Handle StorageBackend changed."""
-        logging.debug("StorageBackendProvides on_changed")
-        if self.remote_ready():
-            self.on.api_ready.emit()
-
-    def _on_storage_backend_relation_broken(self, event):
-        """Handle StorageBackend broken."""
-        logging.debug("RabbitMQStorageBackendProvides on_departed")
-        # TODO clear data on the relation
diff --git a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644
index c8d2e0b1..00000000
--- a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
+++ /dev/null
@@ -1,211 +0,0 @@
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 9
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-    "path-routes",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(missing),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
diff --git a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/cinder-k8s/osci.yaml b/charms/cinder-k8s/osci.yaml
deleted file mode 100644
index 13dbaa2d..00000000
--- a/charms/cinder-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: cinder-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/cinder-k8s/pyproject.toml b/charms/cinder-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/cinder-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/cinder-k8s/rename.sh b/charms/cinder-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/cinder-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/cinder-k8s/requirements.txt b/charms/cinder-k8s/requirements.txt
index c92b43df..7743a6fe 100644
--- a/charms/cinder-k8s/requirements.txt
+++ b/charms/cinder-k8s/requirements.txt
@@ -11,9 +11,11 @@ pydantic<2.0
 lightkube
 lightkube-models
 ops
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 
 git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
 
 # TODO
 requests # Drop - not needed in storage backend interface.
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/cinder-k8s/src/templates/parts/section-database b/charms/cinder-k8s/src/templates/parts/section-database
deleted file mode 100644
index eb52f65e..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/cinder/cinder.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/cinder-k8s/src/templates/parts/section-federation b/charms/cinder-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/cinder-k8s/src/templates/parts/section-identity b/charms/cinder-k8s/src/templates/parts/section-identity
deleted file mode 100644
index cbb1d069..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,24 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/cinder-k8s/src/templates/parts/section-middleware b/charms/cinder-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/cinder-k8s/src/templates/parts/section-service-user b/charms/cinder-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/cinder-k8s/src/templates/parts/section-signing b/charms/cinder-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/cinder-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/cinder-k8s/test-requirements.txt b/charms/cinder-k8s/test-requirements.txt
deleted file mode 100644
index a9b0d698..00000000
--- a/charms/cinder-k8s/test-requirements.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/cinder-k8s/tests/unit/test_cinder_charm.py b/charms/cinder-k8s/tests/unit/test_cinder_charm.py
index 712e21d9..4ff7d581 100644
--- a/charms/cinder-k8s/tests/unit/test_cinder_charm.py
+++ b/charms/cinder-k8s/tests/unit/test_cinder_charm.py
@@ -16,9 +16,8 @@
 
 """Unit tests for core Cinder charm class."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _CinderOperatorCharm(charm.CinderOperatorCharm):
diff --git a/charms/cinder-k8s/tox.ini b/charms/cinder-k8s/tox.ini
deleted file mode 100644
index a411adb3..00000000
--- a/charms/cinder-k8s/tox.ini
+++ /dev/null
@@ -1,160 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/designate-bind-k8s/.gitignore b/charms/designate-bind-k8s/.gitignore
deleted file mode 100644
index 24ff2e41..00000000
--- a/charms/designate-bind-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
diff --git a/charms/designate-bind-k8s/.gitreview b/charms/designate-bind-k8s/.gitreview
deleted file mode 100644
index 15594b28..00000000
--- a/charms/designate-bind-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-designate-bind-k8s.git
-defaultbranch=main
diff --git a/charms/designate-bind-k8s/.stestr.conf b/charms/designate-bind-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/designate-bind-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/designate-bind-k8s/.zuul.yaml b/charms/designate-bind-k8s/.zuul.yaml
deleted file mode 100644
index ef9a2ff4..00000000
--- a/charms/designate-bind-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: designate-bind-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/designate-bind-k8s/charmcraft.yaml b/charms/designate-bind-k8s/charmcraft.yaml
index 9c5a769f..e24ab1f5 100644
--- a/charms/designate-bind-k8s/charmcraft.yaml
+++ b/charms/designate-bind-k8s/charmcraft.yaml
@@ -27,4 +27,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/designate-bind-k8s/fetch-libs.sh b/charms/designate-bind-k8s/fetch-libs.sh
deleted file mode 100755
index d13dfc5c..00000000
--- a/charms/designate-bind-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.observability-libs.v1.kubernetes_service_patch
diff --git a/charms/designate-bind-k8s/osci.yaml b/charms/designate-bind-k8s/osci.yaml
deleted file mode 100644
index 4cc088c3..00000000
--- a/charms/designate-bind-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: designate-bind-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 9/edge
diff --git a/charms/designate-bind-k8s/pyproject.toml b/charms/designate-bind-k8s/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/designate-bind-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/designate-bind-k8s/rename.sh b/charms/designate-bind-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/designate-bind-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/designate-bind-k8s/requirements.txt b/charms/designate-bind-k8s/requirements.txt
index cb87fb03..ccdfbaf8 100644
--- a/charms/designate-bind-k8s/requirements.txt
+++ b/charms/designate-bind-k8s/requirements.txt
@@ -1,5 +1,7 @@
 ops
 jinja2
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 lightkube-models
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/designate-bind-k8s/test-requirements.txt b/charms/designate-bind-k8s/test-requirements.txt
deleted file mode 100644
index d1a61d34..00000000
--- a/charms/designate-bind-k8s/test-requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
diff --git a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py
index 90562b60..5bfb0074 100644
--- a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py
+++ b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py
@@ -14,9 +14,8 @@
 
 """Unit tests."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _BindTestOperatorCharm(charm.BindOperatorCharm):
diff --git a/charms/designate-bind-k8s/tox.ini b/charms/designate-bind-k8s/tox.ini
deleted file mode 100644
index b14f9098..00000000
--- a/charms/designate-bind-k8s/tox.ini
+++ /dev/null
@@ -1,166 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-project_lib_path = {toxinidir}/lib/charms/designate_bind_k8s
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path} {[vars]project_lib_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/designate-k8s/.gitignore b/charms/designate-k8s/.gitignore
deleted file mode 100644
index 24ff2e41..00000000
--- a/charms/designate-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
diff --git a/charms/designate-k8s/.gitreview b/charms/designate-k8s/.gitreview
deleted file mode 100644
index 724badba..00000000
--- a/charms/designate-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-designate-k8s.git
-defaultbranch=main
diff --git a/charms/designate-k8s/.stestr.conf b/charms/designate-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/designate-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/designate-k8s/.zuul.yaml b/charms/designate-k8s/.zuul.yaml
deleted file mode 100644
index 210ad0b5..00000000
--- a/charms/designate-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: designate-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/designate-k8s/charmcraft.yaml b/charms/designate-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/designate-k8s/charmcraft.yaml
+++ b/charms/designate-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/designate-k8s/fetch-libs.sh b/charms/designate-k8s/fetch-libs.sh
deleted file mode 100755
index 5cdf501d..00000000
--- a/charms/designate-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
-charmcraft fetch-lib charms.designate_bind_k8s.v0.bind_rndc
-
diff --git a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py
deleted file mode 100644
index 2bb2f93e..00000000
--- a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py
+++ /dev/null
@@ -1,364 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-"""BindRndc Provides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the bind_rndc interface.
-Import `BindRndcRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "dns-backend"
-Two events are also available to respond to:
-    - bind_rndc_ready
-    - goneaway
-A basic example showing the usage of this relation follows:
-```
-from charms.designate_bind_k8s.v0.bind_rndc import (
-    BindRndcRequires
-)
-class BindRndcClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # BindRndc Requires
-        self.bind_rndc = BindRndcRequires(
-            self, "dns-backend"
-        )
-        self.framework.observe(
-            self.bind_rndc.on.bind_rndc_ready,
-            self._on_bind_rndc_ready
-        )
-        self.framework.observe(
-            self.bind_rndc.on.goneaway,
-            self._on_bind_rndc_goneaway
-        )
-    def _on_bind_rndc_connected(self, event):
-        '''React to the Bind Rndc Connected event.
-        This event happens when BindRndc relation is added to the
-        model.
-        '''
-        # Request the rndc key from the Bind Rndc relation.
-        self.bind_rndc.request_rndc_key("generated nonce")
-    def _on_bind_rndc_ready(self, event):
-        '''React to the Bind Rndc Ready event.
-        This event happens when BindRndc relation is added to the
-        model, relation is ready and/or relation data is changed.
-        '''
-        # Do something with the configuration provided by relation.
-        pass
-    def _on_bind_rndc_goneaway(self, event):
-        '''React to the BindRndc goneaway event.
-        This event happens when BindRndc relation is removed.
-        '''
-        # BindRndc Relation has goneaway.
-        pass
-```
-"""
-
-import json
-import logging
-from typing import (
-    Any,
-    Dict,
-    List,
-    Optional,
-    Union,
-)
-
-import ops
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "1cb766c981874e7383d17cf54148b3d4"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-class BindRndcConnectedEvent(ops.EventBase):
-    """Bind rndc connected event."""
-
-    def __init__(
-        self,
-        handle: ops.Handle,
-        relation_id: int,
-        relation_name: str,
-    ):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-
-    def snapshot(self) -> dict:
-        """Return snapshot data that should be persisted."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-        }
-
-    def restore(self, snapshot: Dict[str, Any]):
-        """Restore the value state from a given snapshot."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-
-
-class BindRndcReadyEvent(ops.EventBase):
-    """Bind rndc ready event."""
-
-    def __init__(
-        self,
-        handle: ops.Handle,
-        relation_id: int,
-        relation_name: str,
-    ):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-
-    def snapshot(self) -> dict:
-        """Return snapshot data that should be persisted."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-        }
-
-    def restore(self, snapshot: Dict[str, Any]):
-        """Restore the value state from a given snapshot."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-
-
-class BindRndcGoneAwayEvent(ops.EventBase):
-    """Bind rndc gone away event."""
-
-    pass
-
-
-class BindRndcRequirerEvents(ops.ObjectEvents):
-    """List of events that the BindRndc requires charm can leverage."""
-
-    connected = ops.EventSource(BindRndcConnectedEvent)
-    ready = ops.EventSource(BindRndcReadyEvent)
-    goneaway = ops.EventSource(BindRndcGoneAwayEvent)
-
-
-class BindRndcRequires(ops.Object):
-    """Class to be instantiated by the requiring side of the relation."""
-
-    on = BindRndcRequirerEvents()
-
-    def __init__(self, charm: ops.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_relation_broken,
-        )
-
-    def _on_relation_joined(self, event: ops.RelationJoinedEvent):
-        """Handle relation joined event."""
-        self.on.connected.emit(
-            event.relation.id,
-            event.relation.name,
-        )
-
-    def _on_relation_changed(self, event: ops.RelationJoinedEvent):
-        """Handle relation changed event."""
-        host = self.host(event.relation)
-        rndc_key = self.get_rndc_key(event.relation)
-
-        if all((host, rndc_key)):
-            self.on.ready.emit(
-                event.relation.id,
-                event.relation.name,
-            )
-
-    def _on_relation_broken(self, event: ops.RelationBrokenEvent):
-        """Handle relation broken event."""
-        self.on.goneaway.emit()
-
-    def host(self, relation: ops.Relation) -> Optional[str]:
-        """Return host from relation."""
-        if relation.app is None:
-            return None
-        return relation.data[relation.app].get("host")
-
-    def nonce(self, relation: ops.Relation) -> Optional[str]:
-        """Return nonce from relation."""
-        return relation.data[self.charm.unit].get("nonce")
-
-    def get_rndc_key(self, relation: ops.Relation) -> Optional[dict]:
-        """Get rndc keys."""
-        if relation.app is None:
-            return None
-        if self.nonce(relation) is None:
-            logger.debug("No nonce set for unit yet")
-            return None
-
-        return json.loads(
-            relation.data[relation.app].get("rndc_keys", "{}")
-        ).get(self.nonce(relation))
-
-    def request_rndc_key(self, relation: ops.Relation, nonce: str):
-        """Request rndc key over the relation."""
-        relation.data[self.charm.unit]["nonce"] = nonce
-
-
-class NewBindClientAttachedEvent(ops.EventBase):
-    """New bind client attached event."""
-
-    def __init__(
-        self,
-        handle: ops.Handle,
-        relation_id: int,
-        relation_name: str,
-    ):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-
-    def snapshot(self) -> dict:
-        """Return snapshot data that should be persisted."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-        }
-
-    def restore(self, snapshot: Dict[str, Any]):
-        """Restore the value state from a given snapshot."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-
-
-class BindClientUpdatedEvent(ops.EventBase):
-    """Bind client updated event."""
-
-    def __init__(
-        self,
-        handle: ops.Handle,
-        relation_id: int,
-        relation_name: str,
-    ):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-
-    def snapshot(self) -> dict:
-        """Return snapshot data that should be persisted."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-        }
-
-    def restore(self, snapshot: Dict[str, Any]):
-        """Restore the value state from a given snapshot."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-
-
-class BindRndcProviderEvents(ops.ObjectEvents):
-    """List of events that the BindRndc provider charm can leverage."""
-
-    new_bind_client_attached = ops.EventSource(NewBindClientAttachedEvent)
-    bind_client_updated = ops.EventSource(BindClientUpdatedEvent)
-
-
-class BindRndcProvides(ops.Object):
-    """Class to be instantiated by the providing side of the relation."""
-
-    on = BindRndcProviderEvents()
-
-    def __init__(self, charm: ops.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _on_relation_joined(self, event: ops.RelationJoinedEvent):
-        self.on.new_bind_client_attached.emit(
-            event.relation.id, event.relation.name
-        )
-
-    def _on_relation_changed(self, event: ops.RelationChangedEvent):
-        self.on.bind_client_updated.emit(
-            event.relation.id, event.relation.name
-        )
-
-    def set_host(self, relation: ops.Relation, host: str):
-        """Set host on the relation."""
-        if not self.charm.unit.is_leader():
-            logger.debug("Not leader, skipping set_host")
-            return
-        relation.data[self.charm.app]["host"] = host
-
-    def get_rndc_keys(self, relation: ops.Relation) -> dict:
-        """Get rndc keys."""
-        return json.loads(relation.data[self.charm.app].get("rndc_keys", "{}"))
-
-    def set_rndc_client_key(
-        self,
-        relation: ops.Relation,
-        client: str,
-        algorithm: str,
-        secret: ops.Secret,
-    ):
-        """Add rndc key to the relation.
-
-        `rndc_keys` is a dict of dicts, keyed by client name. Each client
-        has an algorithm and secret property. The secret is a Juju secret id,
-        containing the actual secret needed to communicate over rndc.
-        """
-        if not self.charm.unit.is_leader():
-            logger.debug("Not leader, skipping set_rndc_client_key")
-            return
-
-        keys = self.get_rndc_keys(relation)
-        keys[client] = {
-            "algorithm": algorithm,
-            "secret": secret.id,
-        }
-
-        relation.data[self.charm.app]["rndc_keys"] = json.dumps(
-            keys, sort_keys=True
-        )
-
-    def remove_rndc_client_key(
-        self,
-        relation: ops.Relation,
-        client: Union[str, List[str]],
-    ):
-        """Remove rndc key from the relation."""
-        if not self.charm.unit.is_leader():
-            logger.debug("Not leader, skipping remove_rndc_client_key")
-            return
-        if isinstance(client, str):
-            client = [client]
-        keys = self.get_rndc_keys(relation)
-        for c in client:
-            keys.pop(c)
-        relation.data[self.charm.app]["rndc_keys"] = json.dumps(
-            keys, sort_keys=True
-        )
diff --git a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/designate-k8s/osci.yaml b/charms/designate-k8s/osci.yaml
deleted file mode 100644
index 9d3005c7..00000000
--- a/charms/designate-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: designate-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/designate-k8s/pyproject.toml b/charms/designate-k8s/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/designate-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/designate-k8s/rename.sh b/charms/designate-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/designate-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/designate-k8s/requirements.txt b/charms/designate-k8s/requirements.txt
index db1e74a8..e0ef265f 100644
--- a/charms/designate-k8s/requirements.txt
+++ b/charms/designate-k8s/requirements.txt
@@ -2,5 +2,7 @@ ops
 jsonschema
 pydantic<2.0
 jinja2
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/designate-k8s/src/templates/parts/database-connection b/charms/designate-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 1fd70ce2..00000000
--- a/charms/designate-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/designate-k8s/src/templates/parts/identity-data b/charms/designate-k8s/src/templates/parts/identity-data
deleted file mode 100644
index 706d9d13..00000000
--- a/charms/designate-k8s/src/templates/parts/identity-data
+++ /dev/null
@@ -1,23 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/designate-k8s/src/templates/parts/section-database b/charms/designate-k8s/src/templates/parts/section-database
deleted file mode 100644
index b060b139..00000000
--- a/charms/designate-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,4 +0,0 @@
-[database]
-{% include "parts/database-connection" %}
-connection_recycle_time = 200
-db_auto_create = false
diff --git a/charms/designate-k8s/src/templates/parts/section-federation b/charms/designate-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/designate-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/designate-k8s/src/templates/parts/section-identity b/charms/designate-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 7568a9a4..00000000
--- a/charms/designate-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,2 +0,0 @@
-[keystone_authtoken]
-{% include "parts/identity-data" %}
diff --git a/charms/designate-k8s/src/templates/parts/section-middleware b/charms/designate-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/designate-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/designate-k8s/src/templates/parts/section-service-user b/charms/designate-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/designate-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/designate-k8s/src/templates/parts/section-signing b/charms/designate-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/designate-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/designate-k8s/test-requirements.txt b/charms/designate-k8s/test-requirements.txt
deleted file mode 100644
index f33eee5a..00000000
--- a/charms/designate-k8s/test-requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
-pytest-mock
diff --git a/charms/designate-k8s/tests/unit/test_designate_charm.py b/charms/designate-k8s/tests/unit/test_designate_charm.py
index 85a55ac3..05c2367d 100644
--- a/charms/designate-k8s/tests/unit/test_designate_charm.py
+++ b/charms/designate-k8s/tests/unit/test_designate_charm.py
@@ -18,13 +18,12 @@
 
 import json
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from ops.testing import (
     Harness,
 )
 
-import charm
-
 
 class _DesignateTestOperatorCharm(charm.DesignateOperatorCharm):
     """Test Operator Charm for Designate Operator."""
diff --git a/charms/designate-k8s/tox.ini b/charms/designate-k8s/tox.ini
deleted file mode 100644
index 067963f9..00000000
--- a/charms/designate-k8s/tox.ini
+++ /dev/null
@@ -1,165 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/glance-k8s/.flake8 b/charms/glance-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/glance-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/glance-k8s/.gitignore b/charms/glance-k8s/.gitignore
deleted file mode 100644
index 2d3a0ccd..00000000
--- a/charms/glance-k8s/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-venv/
-build/
-.stestr/
-*.charm
-.tox
-.coverage
-__pycache__/
-*.py[cod]
-tempest.log
diff --git a/charms/glance-k8s/.gitreview b/charms/glance-k8s/.gitreview
deleted file mode 100644
index 48ce75b3..00000000
--- a/charms/glance-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-glance-k8s.git
-defaultbranch=main
diff --git a/charms/glance-k8s/.jujuignore b/charms/glance-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/glance-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/glance-k8s/.stestr.conf b/charms/glance-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/glance-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/glance-k8s/.zuul.yaml b/charms/glance-k8s/.zuul.yaml
deleted file mode 100644
index 38b2ac8d..00000000
--- a/charms/glance-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: glance-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/glance-k8s/charmcraft.yaml b/charms/glance-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/glance-k8s/charmcraft.yaml
+++ b/charms/glance-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/glance-k8s/fetch-libs.sh b/charms/glance-k8s/fetch-libs.sh
deleted file mode 100755
index e7772471..00000000
--- a/charms/glance-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/glance-k8s/osci.yaml b/charms/glance-k8s/osci.yaml
deleted file mode 100644
index 538d747e..00000000
--- a/charms/glance-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: glance-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/glance-k8s/pyproject.toml b/charms/glance-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/glance-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/glance-k8s/rename.sh b/charms/glance-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/glance-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/glance-k8s/requirements.txt b/charms/glance-k8s/requirements.txt
index c9d578be..451445a1 100644
--- a/charms/glance-k8s/requirements.txt
+++ b/charms/glance-k8s/requirements.txt
@@ -13,11 +13,12 @@ lightkube-models
 ops
 netifaces
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
-
 git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
 
 # Note: Required for cinder-ceph-k8s, glance-k8s
 git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
 # Charmhelpers is only present as interface_ceph_client uses it.
 git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py
index 62c6eb03..106207c0 100755
--- a/charms/glance-k8s/src/charm.py
+++ b/charms/glance-k8s/src/charm.py
@@ -338,7 +338,8 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
 
     def configure_charm(self, event) -> None:
         """Catchall handler to configure charm services."""
-        if not self.relation_handlers_ready():
+        not_ready_relations = self.get_mandatory_relations_not_ready(event)
+        if not_ready_relations:
             logger.debug("Deferring configuration, charm relations not ready")
             return
 
diff --git a/charms/glance-k8s/src/templates/parts/section-database b/charms/glance-k8s/src/templates/parts/section-database
deleted file mode 100644
index eb52f65e..00000000
--- a/charms/glance-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/cinder/cinder.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/glance-k8s/src/templates/parts/section-federation b/charms/glance-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/glance-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/glance-k8s/src/templates/parts/section-identity b/charms/glance-k8s/src/templates/parts/section-identity
deleted file mode 100644
index cbb1d069..00000000
--- a/charms/glance-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,24 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/glance-k8s/src/templates/parts/section-middleware b/charms/glance-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/glance-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-notifications b/charms/glance-k8s/src/templates/parts/section-oslo-notifications
deleted file mode 100644
index ce559feb..00000000
--- a/charms/glance-k8s/src/templates/parts/section-oslo-notifications
+++ /dev/null
@@ -1,4 +0,0 @@
-{% if options.enable_telemetry_notifications -%}
-[oslo_messaging_notifications]
-driver = messagingv2
-{%- endif %}
diff --git a/charms/glance-k8s/src/templates/parts/section-service-user b/charms/glance-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/glance-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/glance-k8s/src/templates/parts/section-signing b/charms/glance-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/glance-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/glance-k8s/test-requirements.txt b/charms/glance-k8s/test-requirements.txt
deleted file mode 100644
index a9b0d698..00000000
--- a/charms/glance-k8s/test-requirements.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/glance-k8s/tests/unit/test_glance_charm.py b/charms/glance-k8s/tests/unit/test_glance_charm.py
index 3cd6887c..8d5c3914 100644
--- a/charms/glance-k8s/tests/unit/test_glance_charm.py
+++ b/charms/glance-k8s/tests/unit/test_glance_charm.py
@@ -16,13 +16,12 @@
 
 """Tests for glance charm."""
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from mock import (
     patch,
 )
 
-import charm
-
 
 class _GlanceOperatorCharm(charm.GlanceOperatorCharm):
     def __init__(self, framework):
diff --git a/charms/glance-k8s/tox.ini b/charms/glance-k8s/tox.ini
deleted file mode 100644
index a411adb3..00000000
--- a/charms/glance-k8s/tox.ini
+++ /dev/null
@@ -1,160 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/gnocchi-k8s/.gitignore b/charms/gnocchi-k8s/.gitignore
deleted file mode 100644
index 7d5f287a..00000000
--- a/charms/gnocchi-k8s/.gitignore
+++ /dev/null
@@ -1,12 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
-
diff --git a/charms/gnocchi-k8s/.gitreview b/charms/gnocchi-k8s/.gitreview
deleted file mode 100644
index 9b24204b..00000000
--- a/charms/gnocchi-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-gnocchi-k8s.git
-defaultbranch=main
diff --git a/charms/gnocchi-k8s/.stestr.conf b/charms/gnocchi-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/gnocchi-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/gnocchi-k8s/.zuul.yaml b/charms/gnocchi-k8s/.zuul.yaml
deleted file mode 100644
index 4daf525d..00000000
--- a/charms/gnocchi-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: gnocchi-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.26-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/gnocchi-k8s/charmcraft.yaml b/charms/gnocchi-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/gnocchi-k8s/charmcraft.yaml
+++ b/charms/gnocchi-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/gnocchi-k8s/fetch-libs.sh b/charms/gnocchi-k8s/fetch-libs.sh
deleted file mode 100755
index e7772471..00000000
--- a/charms/gnocchi-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py
deleted file mode 100644
index fbc679d5..00000000
--- a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py
+++ /dev/null
@@ -1,205 +0,0 @@
-"""GnocchiService Provides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the Gnocchi service interface.
-
-Import `GnocchiServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "gnocchi-db"
-
-Two events are also available to respond to:
-    - readiness_changed
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.gnocchi_k8s.v0.gnocchi_service import (
-    GnocchiServiceRequires
-)
-
-class GnocchiServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        #  GnocchiService Requires
-        self.gnocchi_svc = GnocchiServiceRequires(
-            self, "gnocchi-db",
-        )
-        self.framework.observe(
-            self.gnocchi_svc.on.readiness_changed,
-            self._on_gnocchi_service_readiness_changed
-        )
-        self.framework.observe(
-            self.gnocchi_svc.on.goneaway,
-            self._on_gnocchi_service_goneaway
-        )
-
-    def _on_gnocchi_service_readiness_changed(self, event):
-        '''React to the Gnocchi service readiness changed event.
-
-        This event happens when Gnocchi service relation is added to the
-        model and relation data is changed.
-        '''
-        # Do something with the configuration provided by relation.
-        pass
-
-    def _on_gnocchi_service_goneaway(self, event):
-        '''React to the Gnocchi Service goneaway event.
-
-        This event happens when Gnocchi service relation is removed.
-        '''
-        # HeatSharedConfig Relation has goneaway.
-        pass
-```
-"""
-
-
-import json
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-)
-from ops.framework import (
-    EventSource,
-    Object,
-    ObjectEvents,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "97b7682b415040f3b32d77fff8d93e7e"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 2
-
-
-class GnocchiServiceReadinessRequestEvent(RelationEvent):
-    """GnocchiServiceReadinessRequest Event."""
-
-    pass
-
-
-class GnocchiServiceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    service_readiness = EventSource(GnocchiServiceReadinessRequestEvent)
-
-
-class GnocchiServiceProvides(Object):
-    """GnocchiServiceProvides class."""
-
-    on = GnocchiServiceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _on_relation_changed(self, event: RelationChangedEvent):
-        """Handle Gnocchi service relation changed."""
-        logging.debug("Gnocchi Service relation changed")
-        self.on.service_readiness.emit(event.relation)
-
-    def set_service_status(self, relation: Relation, is_ready: bool) -> None:
-        """Set gnocchi service readiness status on the relation."""
-        if not self.charm.unit.is_leader():
-            logging.debug("Not a leader unit, skipping setting ready status")
-            return
-
-        logging.debug(
-            f"Setting ready status on relation {relation.app.name} "
-            f"{relation.name}/{relation.id}"
-        )
-        relation.data[self.charm.app]["ready"] = json.dumps(is_ready)
-
-
-class GnocchiServiceReadinessChangedEvent(RelationEvent):
-    """GnocchiServiceReadinessChanged Event."""
-
-    pass
-
-
-class GnocchiServiceGoneAwayEvent(RelationEvent):
-    """GnocchiServiceGoneAway Event."""
-
-    pass
-
-
-class GnocchiServiceRequirerEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    readiness_changed = EventSource(GnocchiServiceReadinessChangedEvent)
-    goneaway = EventSource(GnocchiServiceGoneAwayEvent)
-
-
-class GnocchiServiceRequires(Object):
-    """GnocchiServiceRequires class."""
-
-    on = GnocchiServiceRequirerEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_relation_broken,
-        )
-
-    def _on_relation_changed(self, event: RelationChangedEvent):
-        """Handle Gnocchi Service relation changed."""
-        logging.debug("Gnocchi service readiness data changed")
-        self.on.readiness_changed.emit(event.relation)
-
-    def _on_relation_broken(self, event: RelationBrokenEvent):
-        """Handle Gnocchi Service relation broken."""
-        logging.debug("Gnocchi service on_broken")
-        self.on.goneaway.emit(event.relation)
-
-    @property
-    def _gnocchi_service_rel(self) -> Optional[Relation]:
-        """The gnocchi service relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._gnocchi_service_rel:
-            data = self._gnocchi_service_rel.data[
-                self._gnocchi_service_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    @property
-    def service_ready(self) -> bool:
-        """Return if gnocchi service is ready or not."""
-        is_ready = self.get_remote_app_data("ready")
-        if is_ready:
-            return json.loads(is_ready)
-
-        return False
diff --git a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/gnocchi-k8s/osci.yaml b/charms/gnocchi-k8s/osci.yaml
deleted file mode 100644
index 05207373..00000000
--- a/charms/gnocchi-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: gnocchi-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/gnocchi-k8s/pyproject.toml b/charms/gnocchi-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/gnocchi-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/gnocchi-k8s/rename.sh b/charms/gnocchi-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/gnocchi-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/gnocchi-k8s/requirements.txt b/charms/gnocchi-k8s/requirements.txt
index b932d999..6a4bf90a 100644
--- a/charms/gnocchi-k8s/requirements.txt
+++ b/charms/gnocchi-k8s/requirements.txt
@@ -1,6 +1,5 @@
 ops
 jinja2
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 netifaces
 jsonschema
@@ -8,3 +7,6 @@ pydantic<2.0
 
 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
 git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/gnocchi-k8s/src/templates/parts/database-connection b/charms/gnocchi-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 1fd70ce2..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/gnocchi-k8s/src/templates/parts/identity-data b/charms/gnocchi-k8s/src/templates/parts/identity-data
deleted file mode 100644
index 706d9d13..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/identity-data
+++ /dev/null
@@ -1,23 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-database b/charms/gnocchi-k8s/src/templates/parts/section-database
deleted file mode 100644
index 986d9b10..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,3 +0,0 @@
-[database]
-{% include "parts/database-connection" %}
-connection_recycle_time = 200
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-federation b/charms/gnocchi-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-identity b/charms/gnocchi-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 7568a9a4..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,2 +0,0 @@
-[keystone_authtoken]
-{% include "parts/identity-data" %}
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-middleware b/charms/gnocchi-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-signing b/charms/gnocchi-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/gnocchi-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/gnocchi-k8s/test-requirements.txt b/charms/gnocchi-k8s/test-requirements.txt
deleted file mode 100644
index 276e5bee..00000000
--- a/charms/gnocchi-k8s/test-requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/gnocchi-k8s/tests/unit/test_charm.py b/charms/gnocchi-k8s/tests/unit/test_charm.py
index 2d5da5cc..f0ac97d7 100644
--- a/charms/gnocchi-k8s/tests/unit/test_charm.py
+++ b/charms/gnocchi-k8s/tests/unit/test_charm.py
@@ -16,13 +16,12 @@
 
 """Tests for gnocchi charm."""
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from mock import (
     patch,
 )
 
-import charm
-
 
 class _GnocchiCephOperatorCharm(charm.GnocchiCephOperatorCharm):
     def __init__(self, framework):
diff --git a/charms/gnocchi-k8s/tox.ini b/charms/gnocchi-k8s/tox.ini
deleted file mode 100644
index 067963f9..00000000
--- a/charms/gnocchi-k8s/tox.ini
+++ /dev/null
@@ -1,165 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/heat-k8s/.gitignore b/charms/heat-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/heat-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/heat-k8s/.stestr.conf b/charms/heat-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/heat-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/heat-k8s/.zuul.yaml b/charms/heat-k8s/.zuul.yaml
deleted file mode 100644
index cfece9f8..00000000
--- a/charms/heat-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: heat-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/heat-k8s/charmcraft.yaml b/charms/heat-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/heat-k8s/charmcraft.yaml
+++ b/charms/heat-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/heat-k8s/fetch-libs.sh b/charms/heat-k8s/fetch-libs.sh
deleted file mode 100755
index 600c0b69..00000000
--- a/charms/heat-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route
diff --git a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644
index 4cf26164..00000000
--- a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
+++ /dev/null
@@ -1,408 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# Licensed under the Apache2.0, see LICENCE file in charm source for details.
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-- "self" (the charm itself)
-- config_dict
-
-`config_dict` accepts the following keys:
-- additional-hostnames
-- backend-protocol
-- limit-rps
-- limit-whitelist
-- max-body-size
-- owasp-modsecurity-crs
-- owasp-modsecurity-custom-rules
-- path-routes
-- retry-errors
-- rewrite-enabled
-- rewrite-target
-- service-hostname (required)
-- service-name (required)
-- service-namespace
-- service-port (required)
-- session-cookie-max-age
-- tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {
-        "service-hostname": self.config["external_hostname"],
-        "service-name": self.app.name,
-        "service-port": 80,
-    }
-)
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler, for the relation
-changed event to be properly handled.
-"""
-
-import copy
-import logging
-from typing import Dict
-
-from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-INGRESS_RELATION_NAME = "ingress"
-INGRESS_PROXY_RELATION_NAME = "ingress-proxy"
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 16
-
-LOGGER = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "backend-protocol",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "owasp-modsecurity-custom-rules",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-RELATION_INTERFACES_MAPPINGS = {
-    "service-hostname": "host",
-    "service-name": "name",
-    "service-namespace": "model",
-    "service-port": "port",
-}
-RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values())
-
-
-class IngressAvailableEvent(EventBase):
-    """IngressAvailableEvent custom event.
-
-    This event indicates the Ingress provider is available.
-    """
-
-
-class IngressProxyAvailableEvent(EventBase):
-    """IngressProxyAvailableEvent custom event.
-
-    This event indicates the IngressProxy provider is available.
-    """
-
-
-class IngressBrokenEvent(RelationBrokenEvent):
-    """IngressBrokenEvent custom event.
-
-    This event indicates the Ingress provider is broken.
-    """
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events.
-
-    Attrs:
-        ingress_available: Event to indicate that Ingress is available.
-        ingress_proxy_available: Event to indicate that IngressProxy is available.
-        ingress_broken: Event to indicate that Ingress is broken.
-    """
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_proxy_available = EventSource(IngressProxyAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-
-    Attrs:
-        model: Juju model where the charm is deployed.
-        config_dict: Contains all the configuration options for Ingress.
-    """
-
-    def __init__(self, charm: CharmBase, config_dict: Dict) -> None:
-        """Init function for the IngressRequires class.
-
-        Args:
-            charm: The charm that requires the ingress relation.
-            config_dict: Contains all the configuration options for Ingress.
-        """
-        super().__init__(charm, INGRESS_RELATION_NAME)
-
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
-
-        # Set default values.
-        default_relation_fields = {
-            "service-namespace": self.model.name,
-        }
-        config_dict.update(
-            (key, value)
-            for key, value in default_relation_fields.items()
-            if key not in config_dict or not config_dict[key]
-        )
-
-        self.config_dict = self._convert_to_relation_interface(config_dict)
-
-    @staticmethod
-    def _convert_to_relation_interface(config_dict: Dict) -> Dict:
-        """Create a new relation dict that conforms with charm-relation-interfaces.
-
-        Args:
-            config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces.
-
-        Returns:
-            The Ingress configuration conforming with charm-relation-interfaces.
-        """
-        config_dict = copy.copy(config_dict)
-        config_dict.update(
-            (key, config_dict[old_key])
-            for old_key, key in RELATION_INTERFACES_MAPPINGS.items()
-            if old_key in config_dict and config_dict[old_key]
-        )
-        return config_dict
-
-    def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool:
-        """Check our config dict for errors.
-
-        Args:
-            config_dict: Contains all the configuration options for Ingress.
-            update_only: If the charm needs to update only existing keys.
-
-        Returns:
-            If we need to update the config dict or not.
-        """
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            config_key
-            for config_key in config_dict
-            if config_key
-            not in REQUIRED_INGRESS_RELATION_FIELDS
-            | OPTIONAL_INGRESS_RELATION_FIELDS
-            | RELATION_INTERFACES_MAPPINGS_VALUES
-        ]
-        if unknown:
-            LOGGER.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = tuple(
-                config_key
-                for config_key in REQUIRED_INGRESS_RELATION_FIELDS
-                if config_key not in self.config_dict
-            )
-            if missing:
-                LOGGER.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handle the relation-changed event.
-
-        Args:
-            event: Event triggering the relation-changed hook for the relation.
-        """
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors(config_dict=self.config_dict):
-                return
-            event.relation.data[self.model.app].update(
-                (key, str(self.config_dict[key])) for key in self.config_dict
-            )
-
-    def update_config(self, config_dict: Dict) -> None:
-        """Allow for updates to relation.
-
-        Args:
-            config_dict: Contains all the configuration options for Ingress.
-
-        Attrs:
-            config_dict: Contains all the configuration options for Ingress.
-        """
-        if self.model.unit.is_leader():
-            self.config_dict = self._convert_to_relation_interface(config_dict)
-            if self._config_dict_errors(self.config_dict, update_only=True):
-                return
-            relation = self.model.get_relation(INGRESS_RELATION_NAME)
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressBaseProvides(Object):
-    """Parent class for IngressProvides and IngressProxyProvides.
-
-    Attrs:
-        model: Juju model where the charm is deployed.
-    """
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        """Init function for the IngressProxyProvides class.
-
-        Args:
-            charm: The charm that provides the ingress-proxy relation.
-        """
-        super().__init__(charm, relation_name)
-        self.charm = charm
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handle a change to the ingress/ingress-proxy relation.
-
-        Confirm we have the fields we expect to receive.
-
-        Args:
-            event: Event triggering the relation-changed hook for the relation.
-        """
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        relation_name = event.relation.name
-
-        assert event.app is not None  # nosec
-        if not event.relation.data[event.app]:
-            LOGGER.info(
-                "%s hasn't finished configuring, waiting until relation is changed again.",
-                relation_name,
-            )
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None
-        )
-
-        if missing_fields:
-            LOGGER.warning(
-                "Missing required data fields for %s relation: %s",
-                relation_name,
-                ", ".join(missing_fields),
-            )
-            self.model.unit.status = BlockedStatus(
-                f"Missing fields for {relation_name}: {', '.join(missing_fields)}"
-            )
-
-        if relation_name == INGRESS_RELATION_NAME:
-            # Conform to charm-relation-interfaces.
-            if "name" in ingress_data and "port" in ingress_data:
-                name = ingress_data["name"]
-                port = ingress_data["port"]
-            else:
-                name = ingress_data["service-name"]
-                port = ingress_data["service-port"]
-            event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/"
-
-            # Create an event that our charm can use to decide it's okay to
-            # configure the ingress.
-            self.charm.on.ingress_available.emit()
-        elif relation_name == INGRESS_PROXY_RELATION_NAME:
-            self.charm.on.ingress_proxy_available.emit()
-
-
-class IngressProvides(IngressBaseProvides):
-    """Class containing the functionality for the 'provides' side of the 'ingress' relation.
-
-    Attrs:
-        charm: The charm that provides the ingress relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm: CharmBase) -> None:
-        """Init function for the IngressProvides class.
-
-        Args:
-            charm: The charm that provides the ingress relation.
-        """
-        super().__init__(charm, INGRESS_RELATION_NAME)
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(
-            charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken
-        )
-
-    def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
-        """Handle a relation-broken event in the ingress relation.
-
-        Args:
-            event: Event triggering the relation-broken hook for the relation.
-        """
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit(event.relation)
-
-
-class IngressProxyProvides(IngressBaseProvides):
-    """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation.
-
-    Attrs:
-        charm: The charm that provides the ingress-proxy relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm: CharmBase) -> None:
-        """Init function for the IngressProxyProvides class.
-
-        Args:
-            charm: The charm that provides the ingress-proxy relation.
-        """
-        super().__init__(charm, INGRESS_PROXY_RELATION_NAME)
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(
-            charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed
-        )
diff --git a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/heat-k8s/osci.yaml b/charms/heat-k8s/osci.yaml
deleted file mode 100644
index a971062f..00000000
--- a/charms/heat-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: heat-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/heat-k8s/pyproject.toml b/charms/heat-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/heat-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/heat-k8s/rename.sh b/charms/heat-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/heat-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/heat-k8s/requirements.txt b/charms/heat-k8s/requirements.txt
index 88f53cfa..9bf4ecff 100644
--- a/charms/heat-k8s/requirements.txt
+++ b/charms/heat-k8s/requirements.txt
@@ -1,7 +1,6 @@
 ops
 jinja2
 pwgen
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 pydantic<2.0
 
@@ -9,3 +8,6 @@ pydantic<2.0
 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
 # Charmhelpers is only present as interface_ceph_client uses it.
 git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/heat-k8s/src/templates/parts/section-database b/charms/heat-k8s/src/templates/parts/section-database
deleted file mode 100644
index eb52f65e..00000000
--- a/charms/heat-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/cinder/cinder.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/heat-k8s/src/templates/parts/section-federation b/charms/heat-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/heat-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/heat-k8s/src/templates/parts/section-identity b/charms/heat-k8s/src/templates/parts/section-identity
deleted file mode 100644
index d8b11646..00000000
--- a/charms/heat-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,38 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
-
-
-[trustee]
-auth_type = password
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-user_domain_name = {{ identity_service.service_domain_name }}
diff --git a/charms/heat-k8s/src/templates/parts/section-middleware b/charms/heat-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/heat-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/heat-k8s/src/templates/parts/section-signing b/charms/heat-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/heat-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/heat-k8s/test-requirements.txt b/charms/heat-k8s/test-requirements.txt
deleted file mode 100644
index 23e005bd..00000000
--- a/charms/heat-k8s/test-requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-pwgen
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/heat-k8s/tests/unit/test_heat_charm.py b/charms/heat-k8s/tests/unit/test_heat_charm.py
index 05652c02..d230cb1b 100644
--- a/charms/heat-k8s/tests/unit/test_heat_charm.py
+++ b/charms/heat-k8s/tests/unit/test_heat_charm.py
@@ -22,13 +22,12 @@ from unittest.mock import (
     Mock,
 )
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from ops.testing import (
     Harness,
 )
 
-import charm
-
 
 class _HeatTestOperatorCharm(charm.HeatOperatorCharm):
     """Test Operator Charm for Heat Operator."""
diff --git a/charms/heat-k8s/tox.ini b/charms/heat-k8s/tox.ini
deleted file mode 100644
index 848869a4..00000000
--- a/charms/heat-k8s/tox.ini
+++ /dev/null
@@ -1,170 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-    extras
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/horizon-k8s/.flake8 b/charms/horizon-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/horizon-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/horizon-k8s/.gitignore b/charms/horizon-k8s/.gitignore
deleted file mode 100644
index 2b0b5a83..00000000
--- a/charms/horizon-k8s/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-venv/
-build/
-*.charm
-.tox
-.coverage
-__pycache__/
-*.py[cod]
-.stestr/
diff --git a/charms/horizon-k8s/.gitreview b/charms/horizon-k8s/.gitreview
deleted file mode 100644
index dbdc5803..00000000
--- a/charms/horizon-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-horizon-k8s.git
-defaultbranch=main
diff --git a/charms/horizon-k8s/.jujuignore b/charms/horizon-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/horizon-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/horizon-k8s/.stestr.conf b/charms/horizon-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/horizon-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/horizon-k8s/.zuul.yaml b/charms/horizon-k8s/.zuul.yaml
deleted file mode 100644
index fd20909e..00000000
--- a/charms/horizon-k8s/.zuul.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
diff --git a/charms/horizon-k8s/charmcraft.yaml b/charms/horizon-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/horizon-k8s/charmcraft.yaml
+++ b/charms/horizon-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/horizon-k8s/fetch-libs.sh b/charms/horizon-k8s/fetch-libs.sh
deleted file mode 100755
index a276fddb..00000000
--- a/charms/horizon-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.keystone_k8s.v0.identity_credentials
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py
deleted file mode 100644
index 162a46a8..00000000
--- a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py
+++ /dev/null
@@ -1,439 +0,0 @@
-"""IdentityCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_credentials interface.
-
-Import `IdentityCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires
-
-class IdentityCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityCredentials Requires
-        self.identity_credentials = IdentityCredentialsRequires(
-            self, "identity_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_credentials.on.connected, self._on_identity_credentials_connected)
-        self.framework.observe(
-            self.identity_credentials.on.ready, self._on_identity_credentials_ready)
-        self.framework.observe(
-            self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway)
-
-    def _on_identity_credentials_connected(self, event):
-        '''React to the IdentityCredentials connected event.
-
-        This event happens when IdentityCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_credentials_ready(self, event):
-        '''React to the IdentityCredentials ready event.
-
-        The IdentityCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_credentials_goneaway(self, event):
-        '''React to the IdentityCredentials goneaway event.
-
-        This event happens when an IdentityCredentials relation is removed.
-        '''
-        # IdentityCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityCredentialsConnectedEvent(EventBase):
-    """IdentityCredentials connected Event."""
-
-    pass
-
-
-class IdentityCredentialsReadyEvent(EventBase):
-    """IdentityCredentials ready for use Event."""
-
-    pass
-
-
-class IdentityCredentialsGoneAwayEvent(EventBase):
-    """IdentityCredentials relation has gone-away Event"""
-
-    pass
-
-
-class IdentityCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityCredentialsConnectedEvent)
-    ready = EventSource(IdentityCredentialsReadyEvent)
-    goneaway = EventSource(IdentityCredentialsGoneAwayEvent)
-
-
-class IdentityCredentialsRequires(Object):
-    """
-    IdentityCredentialsRequires class
-    """
-
-    on = IdentityCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """IdentityCredentials relation joined."""
-        logging.debug("IdentityCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """IdentityCredentials relation changed."""
-        logging.debug("IdentityCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """IdentityCredentials relation broken."""
-        logging.debug("IdentityCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_credentials_rel(self) -> Relation:
-        """The IdentityCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_credentials_rel.data[self._identity_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def credentials(self) -> str:
-        return self.get_remote_app_data('credentials')
-
-    @property
-    def username(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def password(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the IdentityCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._identity_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasIdentityCredentialsClientsEvent(EventBase):
-    """Has IdentityCredentialsClients Event."""
-
-    pass
-
-
-class ReadyIdentityCredentialsClientsEvent(EventBase):
-    """IdentityCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class IdentityCredentialsClientsGoneAwayEvent(EventBase):
-    """Has IdentityCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class IdentityCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_credentials_clients = EventSource(
-        HasIdentityCredentialsClientsEvent
-    )
-    ready_identity_credentials_clients = EventSource(
-        ReadyIdentityCredentialsClientsEvent
-    )
-    identity_credentials_clients_gone = EventSource(
-        IdentityCredentialsClientsGoneAwayEvent
-    )
-
-
-class IdentityCredentialsProvides(Object):
-    """
-    IdentityCredentialsProvides class
-    """
-
-    on = IdentityCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """Handle IdentityCredentials joined."""
-        logging.debug("IdentityCredentialsProvides on_joined")
-        self.on.has_identity_credentials_clients.emit()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """Handle IdentityCredentials changed."""
-        logging.debug("IdentityCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_identity_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """Handle IdentityCredentials broken."""
-        logging.debug("IdentityCredentialsProvides on_departed")
-        self.on.identity_credentials_clients_gone.emit()
-
-    def set_identity_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              credentials: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str):
-        logging.debug("Setting identity_credentials connection information.")
-        _identity_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_credentials_rel = relation
-        if not _identity_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _identity_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["credentials"] = credentials
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py
deleted file mode 100644
index 9ff0a8d3..00000000
--- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py
+++ /dev/null
@@ -1,439 +0,0 @@
-"""CloudCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the cloud_credentials interface.
-
-Import `CloudCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "cloud_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires
-
-class CloudCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # CloudCredentials Requires
-        self.cloud_credentials = CloudCredentialsRequires(
-            self, "cloud_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.cloud_credentials.on.connected, self._on_cloud_credentials_connected)
-        self.framework.observe(
-            self.cloud_credentials.on.ready, self._on_cloud_credentials_ready)
-        self.framework.observe(
-            self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway)
-
-    def _on_cloud_credentials_connected(self, event):
-        '''React to the CloudCredentials connected event.
-
-        This event happens when n CloudCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_cloud_credentials_ready(self, event):
-        '''React to the CloudCredentials ready event.
-
-        The CloudCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # CloudCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_cloud_credentials_goneaway(self, event):
-        '''React to the CloudCredentials goneaway event.
-
-        This event happens when an CloudCredentials relation is removed.
-        '''
-        # CloudCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "a5d96cc2686c47eea554ce2210c2d24e"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 0
-
-logger = logging.getLogger(__name__)
-
-
-class CloudCredentialsConnectedEvent(EventBase):
-    """CloudCredentials connected Event."""
-
-    pass
-
-
-class CloudCredentialsReadyEvent(EventBase):
-    """CloudCredentials ready for use Event."""
-
-    pass
-
-
-class CloudCredentialsGoneAwayEvent(EventBase):
-    """CloudCredentials relation has gone-away Event"""
-
-    pass
-
-
-class CloudCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(CloudCredentialsConnectedEvent)
-    ready = EventSource(CloudCredentialsReadyEvent)
-    goneaway = EventSource(CloudCredentialsGoneAwayEvent)
-
-
-class CloudCredentialsRequires(Object):
-    """
-    CloudCredentialsRequires class
-    """
-
-    on = CloudCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """CloudCredentials relation joined."""
-        logging.debug("CloudCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """CloudCredentials relation changed."""
-        logging.debug("CloudCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """CloudCredentials relation broken."""
-        logging.debug("CloudCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _cloud_credentials_rel(self) -> Relation:
-        """The CloudCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def credentials(self) -> str:
-        return self.get_remote_app_data('credentials')
-
-    @property
-    def username(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def password(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the CloudCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._cloud_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasCloudCredentialsClientsEvent(EventBase):
-    """Has CloudCredentialsClients Event."""
-
-    pass
-
-
-class ReadyCloudCredentialsClientsEvent(EventBase):
-    """CloudCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class CloudCredentialsClientsGoneAwayEvent(EventBase):
-    """Has CloudCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class CloudCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_cloud_credentials_clients = EventSource(
-        HasCloudCredentialsClientsEvent
-    )
-    ready_cloud_credentials_clients = EventSource(
-        ReadyCloudCredentialsClientsEvent
-    )
-    cloud_credentials_clients_gone = EventSource(
-        CloudCredentialsClientsGoneAwayEvent
-    )
-
-
-class CloudCredentialsProvides(Object):
-    """
-    CloudCredentialsProvides class
-    """
-
-    on = CloudCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """Handle CloudCredentials joined."""
-        logging.debug("CloudCredentialsProvides on_joined")
-        self.on.has_cloud_credentials_clients.emit()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """Handle CloudCredentials changed."""
-        logging.debug("CloudCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_cloud_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """Handle CloudCredentials broken."""
-        logging.debug("CloudCredentialsProvides on_departed")
-        self.on.cloud_credentials_clients_gone.emit()
-
-    def set_cloud_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              credentials: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str):
-        logging.debug("Setting cloud_credentials connection information.")
-        _cloud_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _cloud_credentials_rel = relation
-        if not _cloud_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _cloud_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["credentials"] = credentials
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 35556622..00000000
--- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,518 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 0
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
diff --git a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644
index c3fac4ca..00000000
--- a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - owasp-modsecurity-crs
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 10
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressBrokenEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
-
-    def _on_relation_broken(self, _):
-        """Handle a relation-broken event in the ingress relation."""
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit()
diff --git a/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/horizon-k8s/osci.yaml b/charms/horizon-k8s/osci.yaml
deleted file mode 100644
index 3e23b3e6..00000000
--- a/charms/horizon-k8s/osci.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-- project:
-    templates:
-      - charm-unit-jobs-py38
-      - charm-unit-jobs-py310
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: horizon-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/horizon-k8s/pyproject.toml b/charms/horizon-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/horizon-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/horizon-k8s/rename.sh b/charms/horizon-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/horizon-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/horizon-k8s/requirements.txt b/charms/horizon-k8s/requirements.txt
index 11632a5d..64399ba9 100644
--- a/charms/horizon-k8s/requirements.txt
+++ b/charms/horizon-k8s/requirements.txt
@@ -11,4 +11,6 @@ pydantic<2.0
 lightkube
 lightkube-models
 ops
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/horizon-k8s/src/templates/parts/database-connection b/charms/horizon-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 1fd70ce2..00000000
--- a/charms/horizon-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/horizon-k8s/test-requirements.txt b/charms/horizon-k8s/test-requirements.txt
deleted file mode 100644
index ebee5935..00000000
--- a/charms/horizon-k8s/test-requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/horizon-k8s/tests/unit/test_horizon_charm.py b/charms/horizon-k8s/tests/unit/test_horizon_charm.py
index 48be6733..95a041a3 100644
--- a/charms/horizon-k8s/tests/unit/test_horizon_charm.py
+++ b/charms/horizon-k8s/tests/unit/test_horizon_charm.py
@@ -16,11 +16,10 @@
 
 """Unit tests for Horizon operator."""
 
+import charm
 import mock
 import ops_sunbeam.test_utils as test_utils
 
-import charm
-
 
 class _HorizonOperatorCharm(charm.HorizonOperatorCharm):
     """Test Operator Charm for Horizon Operator."""
diff --git a/charms/horizon-k8s/tox.ini b/charms/horizon-k8s/tox.ini
deleted file mode 100644
index 671cda90..00000000
--- a/charms/horizon-k8s/tox.ini
+++ /dev/null
@@ -1,132 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/keystone-k8s/.flake8 b/charms/keystone-k8s/.flake8
deleted file mode 100644
index c0a92a06..00000000
--- a/charms/keystone-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 80
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/keystone-k8s/.gitignore b/charms/keystone-k8s/.gitignore
deleted file mode 100644
index 4df34f6a..00000000
--- a/charms/keystone-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-.idea/
-*.charm
-.tox
-venv
-.coverage
-__pycache__/
-*.py[cod]
-**.swp
-.stestr/
diff --git a/charms/keystone-k8s/.gitreview b/charms/keystone-k8s/.gitreview
deleted file mode 100644
index ba705e1e..00000000
--- a/charms/keystone-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-keystone-k8s.git
-defaultbranch=main
diff --git a/charms/keystone-k8s/.stestr.conf b/charms/keystone-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/keystone-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/keystone-k8s/.zuul.yaml b/charms/keystone-k8s/.zuul.yaml
deleted file mode 100644
index bfda2e24..00000000
--- a/charms/keystone-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: keystone-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/keystone-k8s/charmcraft.yaml b/charms/keystone-k8s/charmcraft.yaml
index 0c6b3bdb..cbd210b0 100644
--- a/charms/keystone-k8s/charmcraft.yaml
+++ b/charms/keystone-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/keystone-k8s/fetch-libs.sh b/charms/keystone-k8s/fetch-libs.sh
deleted file mode 100755
index a49af7ed..00000000
--- a/charms/keystone-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-#charmcraft fetch-lib charms.sunbeam_keystone_operator.v1.identity_service
-#charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_credentials
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py
deleted file mode 100644
index e3f4565d..00000000
--- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py
+++ /dev/null
@@ -1,458 +0,0 @@
-"""IdentityCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_credentials interface.
-
-Import `IdentityCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires
-
-class IdentityCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityCredentials Requires
-        self.identity_credentials = IdentityCredentialsRequires(
-            self, "identity_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_credentials.on.connected, self._on_identity_credentials_connected)
-        self.framework.observe(
-            self.identity_credentials.on.ready, self._on_identity_credentials_ready)
-        self.framework.observe(
-            self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway)
-
-    def _on_identity_credentials_connected(self, event):
-        '''React to the IdentityCredentials connected event.
-
-        This event happens when IdentityCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_credentials_ready(self, event):
-        '''React to the IdentityCredentials ready event.
-
-        The IdentityCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_credentials_goneaway(self, event):
-        '''React to the IdentityCredentials goneaway event.
-
-        This event happens when an IdentityCredentials relation is removed.
-        '''
-        # IdentityCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityCredentialsConnectedEvent(EventBase):
-    """IdentityCredentials connected Event."""
-
-    pass
-
-
-class IdentityCredentialsReadyEvent(EventBase):
-    """IdentityCredentials ready for use Event."""
-
-    pass
-
-
-class IdentityCredentialsGoneAwayEvent(EventBase):
-    """IdentityCredentials relation has gone-away Event"""
-
-    pass
-
-
-class IdentityCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityCredentialsConnectedEvent)
-    ready = EventSource(IdentityCredentialsReadyEvent)
-    goneaway = EventSource(IdentityCredentialsGoneAwayEvent)
-
-
-class IdentityCredentialsRequires(Object):
-    """
-    IdentityCredentialsRequires class
-    """
-
-    on = IdentityCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """IdentityCredentials relation joined."""
-        logging.debug("IdentityCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """IdentityCredentials relation changed."""
-        logging.debug("IdentityCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """IdentityCredentials relation broken."""
-        logging.debug("IdentityCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_credentials_rel(self) -> Relation:
-        """The IdentityCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_credentials_rel.data[self._identity_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def credentials(self) -> str:
-        return self.get_remote_app_data('credentials')
-
-    @property
-    def username(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def password(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    @property
-    def internal_endpoint(self) -> str:
-        """Return the region for the internal auth url."""
-        return self.get_remote_app_data('internal-endpoint')
-
-    @property
-    def public_endpoint(self) -> str:
-        """Return the region for the public auth url."""
-        return self.get_remote_app_data('public-endpoint')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the IdentityCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._identity_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasIdentityCredentialsClientsEvent(EventBase):
-    """Has IdentityCredentialsClients Event."""
-
-    pass
-
-
-class ReadyIdentityCredentialsClientsEvent(EventBase):
-    """IdentityCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class IdentityCredentialsClientsGoneAwayEvent(EventBase):
-    """Has IdentityCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class IdentityCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_credentials_clients = EventSource(
-        HasIdentityCredentialsClientsEvent
-    )
-    ready_identity_credentials_clients = EventSource(
-        ReadyIdentityCredentialsClientsEvent
-    )
-    identity_credentials_clients_gone = EventSource(
-        IdentityCredentialsClientsGoneAwayEvent
-    )
-
-
-class IdentityCredentialsProvides(Object):
-    """
-    IdentityCredentialsProvides class
-    """
-
-    on = IdentityCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """Handle IdentityCredentials joined."""
-        logging.debug("IdentityCredentialsProvides on_joined")
-        self.on.has_identity_credentials_clients.emit()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """Handle IdentityCredentials changed."""
-        logging.debug("IdentityCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_identity_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """Handle IdentityCredentials broken."""
-        logging.debug("IdentityCredentialsProvides on_departed")
-        self.on.identity_credentials_clients_gone.emit()
-
-    def set_identity_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              credentials: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str,
-                              admin_role: str):
-        logging.debug("Setting identity_credentials connection information.")
-        _identity_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_credentials_rel = relation
-        if not _identity_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _identity_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["credentials"] = credentials
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
-        app_data["internal-endpoint"] = self.charm.internal_endpoint
-        app_data["public-endpoint"] = self.charm.public_endpoint
-        app_data["admin-role"] = admin_role
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
deleted file mode 100644
index 1f10383a..00000000
--- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
+++ /dev/null
@@ -1,393 +0,0 @@
-"""IdentityResourceProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the identity_ops interface.
-
-Import `IdentityResourceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_ops"
-
-Also provide additional parameters to the charm object:
-    - request
-
-Three events are also available to respond to:
-    - provider_ready
-    - provider_goneaway
-    - response_avaialable
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
-
-class IdentityResourceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityResource Requires
-        self.identity_resource = IdentityResourceRequires(
-            self, "identity_ops",
-        )
-        self.framework.observe(
-            self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
-        self.framework.observe(
-            self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
-        self.framework.observe(
-            self.identity_resource.on.response_available, self._on_identity_resource_response)
-
-    def _on_identity_resource_ready(self, event):
-        '''React to the IdentityResource provider_ready event.
-
-        This event happens when n IdentityResource relation is added to the
-        model. Ready to send any ops to keystone.
-        '''
-        # Ready to send any ops.
-        pass
-
-    def _on_identity_resource_response(self, event):
-        '''React to the IdentityResource response_available event.
-
-        The IdentityResource interface will provide the response for the ops sent.
-        '''
-        # Read the response for the ops sent.
-        pass
-
-    def _on_identity_resource_goneaway(self, event):
-        '''React to the IdentityResource goneaway event.
-
-        This event happens when an IdentityResource relation is removed.
-        '''
-        # IdentityResource Relation has goneaway. No ops can be sent.
-        pass
-```
-
-A sample ops request can be of format
-{
-    "id": <request id>
-    "tag": <string to identify request>
-    "ops": [
-        {
-            "name": <op name>,
-            "params": {
-                <param 1>: <value 1>,
-                <param 2>: <value 2>
-            }
-        }
-    ]
-}
-
-For any sensitive data in the ops params, the charm can create secrets and pass
-secret id instead of sensitive data as part of ops request. The charm should
-ensure to grant secret access to provider charm i.e., keystone over relation.
-The secret content should hold the sensitive data with same name as param name.
-"""
-
-import json
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import (
-    EventBase,
-    EventSource,
-    Object,
-    ObjectEvents,
-    StoredState,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b419d4d8249e423487daafc3665ed06f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 4
-
-
-REQUEST_NOT_SENT = 1
-REQUEST_SENT = 2
-REQUEST_PROCESSED = 3
-
-
-class IdentityOpsProviderReadyEvent(RelationEvent):
-    """Has IdentityOpsProviderReady Event."""
-
-    pass
-
-
-class IdentityOpsResponseEvent(RelationEvent):
-    """Has IdentityOpsResponse Event."""
-
-    pass
-
-
-class IdentityOpsProviderGoneAwayEvent(RelationEvent):
-    """Has IdentityOpsProviderGoneAway Event."""
-
-    pass
-
-
-class IdentityResourceResponseEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    provider_ready = EventSource(IdentityOpsProviderReadyEvent)
-    response_available = EventSource(IdentityOpsResponseEvent)
-    provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
-
-
-class IdentityResourceRequires(Object):
-    """IdentityResourceRequires class."""
-
-    on = IdentityResourceResponseEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self._stored.set_default(provider_ready=False, requests=[])
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_resource_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_resource_relation_broken,
-        )
-
-    def _on_identity_resource_relation_joined(
-        self, event: RelationJoinedEvent
-    ):
-        """Handle IdentityResource joined."""
-        self._stored.provider_ready = True
-        self.on.provider_ready.emit(event.relation)
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        id_ = self.response.get("id")
-        self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
-        self.on.response_available.emit(event.relation)
-
-    def _on_identity_resource_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle IdentityResource broken."""
-        self._stored.provider_ready = False
-        self.on.provider_goneaway.emit(event.relation)
-
-    @property
-    def _identity_resource_rel(self) -> Optional[Relation]:
-        """The IdentityResource relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def response(self) -> dict:
-        """Response object from keystone."""
-        response = self.get_remote_app_data("response")
-        if not response:
-            return {}
-
-        try:
-            return json.loads(response)
-        except Exception as e:
-            logger.debug(str(e))
-
-        return {}
-
-    def save_request_in_store(
-        self, id: str, tag: str, ops: list, state: int
-    ) -> None:
-        """Save request in the store."""
-        if id is None:
-            return
-
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                if tag:
-                    request["tag"] = tag
-                if ops:
-                    request["ops"] = ops
-                request["state"] = state
-                return
-
-        # New request
-        self._stored.requests.append(
-            {"id": id, "tag": tag, "ops": ops, "state": state}
-        )
-
-    def get_request_from_store(self, id: str) -> dict:
-        """Get request from the stote."""
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                return request
-
-        return {}
-
-    def is_request_processed(self, id: str) -> bool:
-        """Check if request is processed."""
-        for request in self._stored.requests:
-            if (
-                request.get("id") == id
-                and request.get("state") == REQUEST_PROCESSED
-            ):
-                return True
-
-        return False
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._identity_resource_rel:
-            data = self._identity_resource_rel.data[
-                self._identity_resource_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    def ready(self) -> bool:
-        """Interface is ready or not.
-
-        Interface is considered ready if the op request is processed
-        and response is sent. In case of non leader unit, just consider
-        the interface is ready.
-        """
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, set the interface to ready")
-            return True
-
-        try:
-            app_data = self._identity_resource_rel.data[self.charm.app]
-            if "request" not in app_data:
-                return False
-
-            request = json.loads(app_data["request"])
-            request_id = request.get("id")
-            response_id = self.response.get("id")
-            if request_id == response_id:
-                return True
-        except Exception as e:
-            logger.debug(str(e))
-
-        return False
-
-    def request_ops(self, request: dict) -> None:
-        """Request keystone ops."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending request")
-            return
-
-        id_ = request.get("id")
-        tag = request.get("tag")
-        ops = request.get("ops")
-        req = self.get_request_from_store(id_)
-        if req and req.get("state") == REQUEST_PROCESSED:
-            logger.debug("Request {id_} already processed")
-            return
-
-        if not self._stored.provider_ready:
-            self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
-            logger.debug("Keystone not yet ready to take requests")
-            return
-
-        logger.debug("Requesting ops to keystone")
-        app_data = self._identity_resource_rel.data[self.charm.app]
-        app_data["request"] = json.dumps(request)
-        self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
-
-
-class IdentityOpsRequestEvent(EventBase):
-    """Has IdentityOpsRequest Event."""
-
-    def __init__(self, handle, relation_id, relation_name, request):
-        """Initialise event."""
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.request = request
-
-    def snapshot(self):
-        """Snapshot the event."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "request": self.request,
-        }
-
-    def restore(self, snapshot):
-        """Restore the event."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.request = snapshot["request"]
-
-
-class IdentityResourceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    process_op = EventSource(IdentityOpsRequestEvent)
-
-
-class IdentityResourceProvides(Object):
-    """IdentityResourceProvides class."""
-
-    on = IdentityResourceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        request = event.relation.data[event.relation.app].get("request")
-        if request is not None:
-            self.on.process_op.emit(
-                event.relation.id, event.relation.name, request
-            )
-
-    def set_ops_response(
-        self, relation_id: str, relation_name: str, ops_response: dict
-    ) -> None:
-        """Set response to ops request."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending response")
-            return
-
-        logger.debug("Update response from keystone")
-        _identity_resource_rel = self.charm.model.get_relation(
-            relation_name, relation_id
-        )
-        if not _identity_resource_rel:
-            # Relation has disappeared so skip send of data
-            return
-
-        app_data = _identity_resource_rel.data[self.charm.app]
-        app_data["response"] = json.dumps(ops_response)
diff --git a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644
index c3fac4ca..00000000
--- a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - owasp-modsecurity-crs
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 10
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressBrokenEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
-
-    def _on_relation_broken(self, _):
-        """Handle a relation-broken event in the ingress relation."""
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit()
diff --git a/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/keystone-k8s/osci.yaml b/charms/keystone-k8s/osci.yaml
deleted file mode 100644
index 0d6d4c1c..00000000
--- a/charms/keystone-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: keystone-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/keystone-k8s/pyproject.toml b/charms/keystone-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/keystone-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/keystone-k8s/rename.sh b/charms/keystone-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/keystone-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/keystone-k8s/requirements.txt b/charms/keystone-k8s/requirements.txt
index 1bec875c..e9b413e7 100644
--- a/charms/keystone-k8s/requirements.txt
+++ b/charms/keystone-k8s/requirements.txt
@@ -13,6 +13,7 @@ lightkube-models
 ops
 pwgen
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
-
 python-keystoneclient  # keystone-k8s
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/keystone-k8s/src/charm.py b/charms/keystone-k8s/src/charm.py
index 2f0d0628..9a65ddc2 100755
--- a/charms/keystone-k8s/src/charm.py
+++ b/charms/keystone-k8s/src/charm.py
@@ -71,7 +71,6 @@ from ops.model import (
     SecretNotFoundError,
     SecretRotate,
 )
-
 from utils import (
     manager,
 )
diff --git a/charms/keystone-k8s/src/templates/parts/section-database b/charms/keystone-k8s/src/templates/parts/section-database
deleted file mode 100644
index 88b0abd5..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/keystone/keystone.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/keystone-k8s/src/templates/parts/section-federation b/charms/keystone-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/keystone-k8s/src/templates/parts/section-middleware b/charms/keystone-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications b/charms/keystone-k8s/src/templates/parts/section-oslo-notifications
deleted file mode 100644
index ce559feb..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications
+++ /dev/null
@@ -1,4 +0,0 @@
-{% if options.enable_telemetry_notifications -%}
-[oslo_messaging_notifications]
-driver = messagingv2
-{%- endif %}
diff --git a/charms/keystone-k8s/src/templates/parts/section-signing b/charms/keystone-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/keystone-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/keystone-k8s/src/utils/manager.py b/charms/keystone-k8s/src/utils/manager.py
index 182ae147..91d282f0 100644
--- a/charms/keystone-k8s/src/utils/manager.py
+++ b/charms/keystone-k8s/src/utils/manager.py
@@ -37,7 +37,6 @@ from ops import (
 from ops.model import (
     MaintenanceStatus,
 )
-
 from utils.client import (
     KeystoneClient,
     KeystoneExceptionError,
diff --git a/charms/keystone-k8s/test-requirements.txt b/charms/keystone-k8s/test-requirements.txt
deleted file mode 100644
index 23e005bd..00000000
--- a/charms/keystone-k8s/test-requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-pwgen
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/keystone-k8s/tests/unit/test_keystone_charm.py b/charms/keystone-k8s/tests/unit/test_keystone_charm.py
index 37af2812..96c76ebb 100644
--- a/charms/keystone-k8s/tests/unit/test_keystone_charm.py
+++ b/charms/keystone-k8s/tests/unit/test_keystone_charm.py
@@ -24,11 +24,10 @@ from unittest.mock import (
     MagicMock,
 )
 
+import charm
 import mock
 import ops_sunbeam.test_utils as test_utils
 
-import charm
-
 
 class _KeystoneOperatorCharm(charm.KeystoneOperatorCharm):
     """Create Keystone operator test charm."""
diff --git a/charms/keystone-k8s/tox.ini b/charms/keystone-k8s/tox.ini
deleted file mode 100644
index 57750258..00000000
--- a/charms/keystone-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-  HOME
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series=
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/keystone-ldap-k8s/.gitignore b/charms/keystone-ldap-k8s/.gitignore
deleted file mode 100644
index 4df34f6a..00000000
--- a/charms/keystone-ldap-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-.idea/
-*.charm
-.tox
-venv
-.coverage
-__pycache__/
-*.py[cod]
-**.swp
-.stestr/
diff --git a/charms/keystone-ldap-k8s/.gitreview b/charms/keystone-ldap-k8s/.gitreview
deleted file mode 100644
index 069407b6..00000000
--- a/charms/keystone-ldap-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-keystone-ldap-k8s.git
-defaultbranch=main
diff --git a/charms/keystone-ldap-k8s/.stestr.conf b/charms/keystone-ldap-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/keystone-ldap-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/keystone-ldap-k8s/.zuul.yaml b/charms/keystone-ldap-k8s/.zuul.yaml
deleted file mode 100644
index 17366a2c..00000000
--- a/charms/keystone-ldap-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: keystone-ldap-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/keystone-ldap-k8s/charmcraft.yaml b/charms/keystone-ldap-k8s/charmcraft.yaml
index 8c3cfc6f..12fc2d53 100644
--- a/charms/keystone-ldap-k8s/charmcraft.yaml
+++ b/charms/keystone-ldap-k8s/charmcraft.yaml
@@ -27,4 +27,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py
deleted file mode 100644
index 0d9858c9..00000000
--- a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py
+++ /dev/null
@@ -1,159 +0,0 @@
-"""Interface for passing domain configuration."""
-
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-)
-from ops.framework import (
-    EventSource,
-    Object,
-    ObjectEvents,
-)
-from ops.model import (
-    Relation,
-)
-import base64
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "dfeee73ed0b248c29ed905aeda6fd417"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-class DomainConfigRequestEvent(RelationEvent):
-    """DomainConfigRequest Event."""
-    pass
-
-class DomainConfigProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    remote_ready = EventSource(DomainConfigRequestEvent)
-
-class DomainConfigProvides(Object):
-    """DomainConfigProvides class."""
-
-    on = DomainConfigProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_domain_config_relation_changed,
-        )
-
-    def _on_domain_config_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle DomainConfig relation changed."""
-        logging.debug("DomainConfig relation changed")
-        self.on.remote_ready.emit(event.relation)
-
-    def set_domain_info(
-        self, domain_name: str, config_contents: str, ca=None
-    ) -> None:
-        """Set ceilometer configuration on the relation."""
-        if not self.charm.unit.is_leader():
-            logging.debug("Not a leader unit, skipping set config")
-            return
-        for relation in self.relations:
-            relation.data[self.charm.app]["domain-name"] = domain_name
-            relation.data[self.charm.app]["config-contents"] = base64.b64encode(config_contents.encode()).decode()
-            if ca:
-                relation.data[self.charm.app]["ca"] = base64.b64encode(ca.encode()).decode()
-
-    @property
-    def relations(self):
-        return self.framework.model.relations[self.relation_name]
-
-class DomainConfigChangedEvent(RelationEvent):
-    """DomainConfigChanged Event."""
-
-    pass
-
-
-class DomainConfigGoneAwayEvent(RelationBrokenEvent):
-    """DomainConfigGoneAway Event."""
-
-    pass
-
-
-class DomainConfigRequirerEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    config_changed = EventSource(DomainConfigChangedEvent)
-    goneaway = EventSource(DomainConfigGoneAwayEvent)
-
-
-class DomainConfigRequires(Object):
-    """DomainConfigRequires class."""
-
-    on = DomainConfigRequirerEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_domain_config_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_domain_config_relation_broken,
-        )
-
-    def _on_domain_config_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle DomainConfig relation changed."""
-        logging.debug("DomainConfig config data changed")
-        self.on.config_changed.emit(event.relation)
-
-    def _on_domain_config_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle DomainConfig relation changed."""
-        logging.debug("DomainConfig on_broken")
-        self.on.goneaway.emit(event.relation)
-
-    def get_domain_configs(self, exclude=None):
-        exclude = exclude or []
-        configs = []
-        for relation in self.relations:
-            if relation in exclude:
-                continue
-            try:
-                domain_name = relation.data[relation.app].get("domain-name")
-            except KeyError:
-                logging.debug("Key error accessing app data")
-                continue
-            raw_config_contents = relation.data[relation.app].get("config-contents")
-            if not all([domain_name, raw_config_contents]):
-                continue
-            raw_ca = relation.data[relation.app].get("ca")
-            config = {
-                "domain-name": domain_name,
-                "config-contents": base64.b64decode(raw_config_contents).decode()}
-            if raw_ca:
-                config["ca"] = base64.b64decode(raw_ca).decode()
-            configs.append(config)
-        return configs
-
-    @property
-    def relations(self):
-        return self.framework.model.relations[self.relation_name]
-
diff --git a/charms/keystone-ldap-k8s/osci.yaml b/charms/keystone-ldap-k8s/osci.yaml
deleted file mode 100644
index c5faf352..00000000
--- a/charms/keystone-ldap-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: keystone-ldap-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/keystone-ldap-k8s/pyproject.toml b/charms/keystone-ldap-k8s/pyproject.toml
deleted file mode 100644
index 2edc519a..00000000
--- a/charms/keystone-ldap-k8s/pyproject.toml
+++ /dev/null
@@ -1,33 +0,0 @@
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 99
-target-version = ["py38"]
-
-[tool.isort]
-line_length = 99
-profile = "black"
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 99
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/charms/keystone-ldap-k8s/rename.sh b/charms/keystone-ldap-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/keystone-ldap-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/keystone-ldap-k8s/requirements.txt b/charms/keystone-ldap-k8s/requirements.txt
index d4f4a3d0..f545ded9 100644
--- a/charms/keystone-ldap-k8s/requirements.txt
+++ b/charms/keystone-ldap-k8s/requirements.txt
@@ -12,6 +12,7 @@ lightkube-models
 ops
 pwgen
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
-
 python-keystoneclient  # keystone-k8s
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/keystone-ldap-k8s/src/charm.py b/charms/keystone-ldap-k8s/src/charm.py
index fc4c9ede..26d3ae74 100755
--- a/charms/keystone-ldap-k8s/src/charm.py
+++ b/charms/keystone-ldap-k8s/src/charm.py
@@ -23,7 +23,11 @@ Send domain configuration to the keystone charm.
 """
 import json
 import logging
-from typing import Callable, List, Mapping
+from typing import (
+    Callable,
+    List,
+    Mapping,
+)
 
 import charms.keystone_k8s.v0.domain_config as sunbeam_dc_svc
 import jinja2
@@ -31,7 +35,9 @@ import ops.charm
 import ops_sunbeam.charm as sunbeam_charm
 import ops_sunbeam.config_contexts as config_contexts
 import ops_sunbeam.relation_handlers as sunbeam_rhandlers
-from ops.main import main
+from ops.main import (
+    main,
+)
 
 # Log messages can be retrieved using juju debug-log
 logger = logging.getLogger(__name__)
@@ -94,7 +100,9 @@ class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm):
     def __init__(self, *args):
         super().__init__(*args)
 
-    def get_relation_handlers(self, handlers=None) -> List[sunbeam_rhandlers.RelationHandler]:
+    def get_relation_handlers(
+        self, handlers=None
+    ) -> List[sunbeam_rhandlers.RelationHandler]:
         """Relation handlers for the service."""
         handlers = handlers or []
         if self.can_add_handler(self.DOMAIN_CONFIG_RELATION_NAME, handlers):
diff --git a/charms/keystone-ldap-k8s/test-requirements.txt b/charms/keystone-ldap-k8s/test-requirements.txt
deleted file mode 100644
index 23e005bd..00000000
--- a/charms/keystone-ldap-k8s/test-requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-pwgen
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/keystone-ldap-k8s/tests/integration/test_charm.py b/charms/keystone-ldap-k8s/tests/integration/test_charm.py
index 18f24d39..3911a065 100644
--- a/charms/keystone-ldap-k8s/tests/integration/test_charm.py
+++ b/charms/keystone-ldap-k8s/tests/integration/test_charm.py
@@ -1,14 +1,32 @@
 #!/usr/bin/env python3
-# Copyright 2023 liam
-# See LICENSE file for licensing details.
+
+# 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.
+
+"""Define keystone-ldap integration tests."""
 
 import asyncio
 import logging
-from pathlib import Path
+from pathlib import (
+    Path,
+)
 
 import pytest
 import yaml
-from pytest_operator.plugin import OpsTest
+from pytest_operator.plugin import (
+    OpsTest,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -24,12 +42,21 @@ async def test_build_and_deploy(ops_test: OpsTest):
     """
     # Build and deploy charm from local source folder
     charm = await ops_test.build_charm(".")
-    resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]}
+    resources = {
+        "httpbin-image": METADATA["resources"]["httpbin-image"][
+            "upstream-source"
+        ]
+    }
 
     # Deploy the charm and wait for active/idle status
     await asyncio.gather(
-        ops_test.model.deploy(await charm, resources=resources, application_name=APP_NAME),
+        ops_test.model.deploy(
+            await charm, resources=resources, application_name=APP_NAME
+        ),
         ops_test.model.wait_for_idle(
-            apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
+            apps=[APP_NAME],
+            status="active",
+            raise_on_blocked=True,
+            timeout=1000,
         ),
     )
diff --git a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py
index 07c6a02d..3d0cf6cb 100644
--- a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py
+++ b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py
@@ -19,10 +19,11 @@
 import base64
 import json
 
-import ops_sunbeam.test_utils as test_utils
-from ops.testing import Harness
-
 import charm
+import ops_sunbeam.test_utils as test_utils
+from ops.testing import (
+    Harness,
+)
 
 
 class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm):
@@ -45,6 +46,8 @@ class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm):
 
 
 class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase):
+    """Create Keystone Ldap test charm."""
+
     def setUp(self):
         """Run test setup."""
         self.harness = Harness(charm.KeystoneLDAPK8SCharm)
@@ -56,7 +59,9 @@ class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase):
         self.harness.set_leader()
         rel_id = self.harness.add_relation("domain-config", "keystone")
         self.harness.add_relation_unit(rel_id, "keystone/0")
-        rel_data = self.harness.get_relation_data(rel_id, self.harness.charm.unit.app.name)
+        rel_data = self.harness.get_relation_data(
+            rel_id, self.harness.charm.unit.app.name
+        )
         ldap_config_flags = json.dumps(
             {
                 "group_tree_dn": "ou=groups,dc=test,dc=com",
diff --git a/charms/keystone-ldap-k8s/tox.ini b/charms/keystone-ldap-k8s/tox.ini
deleted file mode 100644
index 57750258..00000000
--- a/charms/keystone-ldap-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-  HOME
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series=
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/magnum-k8s/.flake8 b/charms/magnum-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/magnum-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/magnum-k8s/.gitignore b/charms/magnum-k8s/.gitignore
deleted file mode 100644
index 33d25ac9..00000000
--- a/charms/magnum-k8s/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-.stestr/
diff --git a/charms/magnum-k8s/.gitreview b/charms/magnum-k8s/.gitreview
deleted file mode 100644
index 6bdf9404..00000000
--- a/charms/magnum-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-magnum-k8s.git
-defaultbranch=main
diff --git a/charms/magnum-k8s/.jujuignore b/charms/magnum-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/magnum-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/magnum-k8s/.stestr.conf b/charms/magnum-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/magnum-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/magnum-k8s/.zuul.yaml b/charms/magnum-k8s/.zuul.yaml
deleted file mode 100644
index 5509ca5f..00000000
--- a/charms/magnum-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: magnum-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/magnum-k8s/charmcraft.yaml b/charms/magnum-k8s/charmcraft.yaml
index 3a9d1b7d..ea13253e 100644
--- a/charms/magnum-k8s/charmcraft.yaml
+++ b/charms/magnum-k8s/charmcraft.yaml
@@ -17,4 +17,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/magnum-k8s/fetch-libs.sh b/charms/magnum-k8s/fetch-libs.sh
deleted file mode 100755
index a573dd64..00000000
--- a/charms/magnum-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
deleted file mode 100644
index 6ef944ef..00000000
--- a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
+++ /dev/null
@@ -1,373 +0,0 @@
-"""IdentityResourceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_ops interface.
-
-Import `IdentityResourceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_ops"
-
-Also provide additional parameters to the charm object:
-    - request
-
-Three events are also available to respond to:
-    - provider_ready
-    - provider_goneaway
-    - response_avaialable
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
-
-class IdentityResourceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityResource Requires
-        self.identity_resource = IdentityResourceRequires(
-            self, "identity_ops",
-        )
-        self.framework.observe(
-            self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
-        self.framework.observe(
-            self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
-        self.framework.observe(
-            self.identity_resource.on.response_available, self._on_identity_resource_response)
-
-    def _on_identity_resource_ready(self, event):
-        '''React to the IdentityResource provider_ready event.
-
-        This event happens when n IdentityResource relation is added to the
-        model. Ready to send any ops to keystone.
-        '''
-        # Ready to send any ops.
-        pass
-
-    def _on_identity_resource_response(self, event):
-        '''React to the IdentityResource response_available event.
-
-        The IdentityResource interface will provide the response for the ops sent.
-        '''
-        # Read the response for the ops sent.
-        pass
-
-    def _on_identity_resource_goneaway(self, event):
-        '''React to the IdentityResource goneaway event.
-
-        This event happens when an IdentityResource relation is removed.
-        '''
-        # IdentityResource Relation has goneaway. No ops can be sent.
-        pass
-```
-
-A sample ops request can be of format
-{
-    "id": <request id>
-    "tag": <string to identify request>
-    "ops": [
-        {
-            "name": <op name>,
-            "params": {
-                <param 1>: <value 1>,
-                <param 2>: <value 2>
-            }
-        }
-    ]
-}
-
-For any sensitive data in the ops params, the charm can create secrets and pass
-secret id instead of sensitive data as part of ops request. The charm should
-ensure to grant secret access to provider charm i.e., keystone over relation.
-The secret content should hold the sensitive data with same name as param name.
-"""
-
-import json
-import logging
-
-from ops.charm import (
-    RelationEvent,
-)
-from ops.framework import (
-    EventBase,
-    EventSource,
-    Object,
-    ObjectEvents,
-    StoredState,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b419d4d8249e423487daafc3665ed06f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 2
-
-
-REQUEST_NOT_SENT = 1
-REQUEST_SENT = 2
-REQUEST_PROCESSED = 3
-
-
-class IdentityOpsProviderReadyEvent(RelationEvent):
-    """Has IdentityOpsProviderReady Event."""
-
-    pass
-
-
-class IdentityOpsResponseEvent(RelationEvent):
-    """Has IdentityOpsResponse Event."""
-
-    pass
-
-
-class IdentityOpsProviderGoneAwayEvent(RelationEvent):
-    """Has IdentityOpsProviderGoneAway Event."""
-
-    pass
-
-
-class IdentityResourceResponseEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    provider_ready = EventSource(IdentityOpsProviderReadyEvent)
-    response_available = EventSource(IdentityOpsResponseEvent)
-    provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
-
-
-class IdentityResourceRequires(Object):
-    """IdentityResourceRequires class."""
-
-    on = IdentityResourceResponseEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self._stored.set_default(provider_ready=False, requests=[])
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_resource_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_resource_relation_broken,
-        )
-
-    def _on_identity_resource_relation_joined(self, event):
-        """Handle IdentityResource joined."""
-        self._stored.provider_ready = True
-        self.on.provider_ready.emit(event.relation)
-
-    def _on_identity_resource_relation_changed(self, event):
-        """Handle IdentityResource changed."""
-        id_ = self.response.get("id")
-        self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
-        self.on.response_available.emit(event.relation)
-
-    def _on_identity_resource_relation_broken(self, event):
-        """Handle IdentityResource broken."""
-        self._stored.provider_ready = False
-        self.on.provider_goneaway.emit(event.relation)
-
-    @property
-    def _identity_resource_rel(self) -> Relation:
-        """The IdentityResource relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def response(self) -> dict:
-        """Response object from keystone."""
-        response = self.get_remote_app_data("response")
-        if not response:
-            return {}
-
-        try:
-            return json.loads(response)
-        except Exception as e:
-            logger.debug(str(e))
-
-        return {}
-
-    def save_request_in_store(self, id: str, tag: str, ops: list, state: int):
-        """Save request in the store."""
-        if id is None:
-            return
-
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                if tag:
-                    request["tag"] = tag
-                if ops:
-                    request["ops"] = ops
-                request["state"] = state
-                return
-
-        # New request
-        self._stored.requests.append(
-            {"id": id, "tag": tag, "ops": ops, "state": state}
-        )
-
-    def get_request_from_store(self, id: str) -> dict:
-        """Get request from the stote."""
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                return request
-
-        return {}
-
-    def is_request_processed(self, id: str) -> bool:
-        """Check if request is processed."""
-        for request in self._stored.requests:
-            if (
-                request.get("id") == id
-                and request.get("state") == REQUEST_PROCESSED
-            ):
-                return True
-
-        return False
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_resource_rel.data[
-            self._identity_resource_rel.app
-        ]
-        return data.get(key)
-
-    def ready(self) -> bool:
-        """Interface is ready or not.
-
-        Interface is considered ready if the op request is processed
-        and response is sent. In case of non leader unit, just consider
-        the interface is ready.
-        """
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, set the interface to ready")
-            return True
-
-        try:
-            app_data = self._identity_resource_rel.data[self.charm.app]
-            if "request" not in app_data:
-                return False
-
-            request = json.loads(app_data["request"])
-            request_id = request.get("id")
-            response_id = self.response.get("id")
-            if request_id == response_id:
-                return True
-        except Exception as e:
-            logger.debug(str(e))
-
-        return False
-
-    def request_ops(self, request: dict) -> None:
-        """Request keystone ops."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending request")
-            return
-
-        id_ = request.get("id")
-        tag = request.get("tag")
-        ops = request.get("ops")
-        req = self.get_request_from_store(id_)
-        if req and req.get("state") == REQUEST_PROCESSED:
-            logger.debug("Request {id_} already processed")
-            return
-
-        if not self._stored.provider_ready:
-            self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
-            logger.debug("Keystone not yet ready to take requests")
-            return
-
-        logger.debug("Requesting ops to keystone")
-        app_data = self._identity_resource_rel.data[self.charm.app]
-        app_data["request"] = json.dumps(request)
-        self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
-
-
-class IdentityOpsRequestEvent(EventBase):
-    """Has IdentityOpsRequest Event."""
-
-    def __init__(self, handle, relation_id, relation_name, request):
-        """Initialise event."""
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.request = request
-
-    def snapshot(self):
-        """Snapshot the event."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "request": self.request,
-        }
-
-    def restore(self, snapshot):
-        """Restore the event."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.request = snapshot["request"]
-
-
-class IdentityResourceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    process_op = EventSource(IdentityOpsRequestEvent)
-
-
-class IdentityResourceProvides(Object):
-    """IdentityResourceProvides class."""
-
-    on = IdentityResourceProviderEvents()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-
-    def _on_identity_resource_relation_changed(self, event):
-        """Handle IdentityResource changed."""
-        request = event.relation.data[event.relation.app].get("request", {})
-        self.on.process_op.emit(
-            event.relation.id, event.relation.name, request
-        )
-
-    def set_ops_response(
-        self, relation_id: str, relation_name: str, ops_response: dict
-    ):
-        """Set response to ops request."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending response")
-            return
-
-        logger.debug("Update response from keystone")
-        _identity_resource_rel = self.charm.model.get_relation(
-            relation_name, relation_id
-        )
-        if not _identity_resource_rel:
-            # Relation has disappeared so skip send of data
-            return
-
-        app_data = _identity_resource_rel.data[self.charm.app]
-        app_data["response"] = json.dumps(ops_response)
diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/magnum-k8s/osci.yaml b/charms/magnum-k8s/osci.yaml
deleted file mode 100644
index c5db11f6..00000000
--- a/charms/magnum-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: magnum-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/magnum-k8s/pyproject.toml b/charms/magnum-k8s/pyproject.toml
deleted file mode 100644
index e6de53b4..00000000
--- a/charms/magnum-k8s/pyproject.toml
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
-
diff --git a/charms/magnum-k8s/rename.sh b/charms/magnum-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/magnum-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/magnum-k8s/requirements.txt b/charms/magnum-k8s/requirements.txt
index 1dfcf975..20492790 100644
--- a/charms/magnum-k8s/requirements.txt
+++ b/charms/magnum-k8s/requirements.txt
@@ -1,6 +1,8 @@
 ops
 jinja2
 pydantic<2.0
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
 pwgen
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/magnum-k8s/src/templates/magnum.conf.j2 b/charms/magnum-k8s/src/templates/magnum.conf.j2
index 6ef7b2ba..aa67c6da 100644
--- a/charms/magnum-k8s/src/templates/magnum.conf.j2
+++ b/charms/magnum-k8s/src/templates/magnum.conf.j2
@@ -5,11 +5,16 @@ state_path = /var/lib/magnum
 
 transport_url = {{ amqp.transport_url }}
 
-{% include "parts/database-connection" %}
+{% if database.connection -%}
+sql_connection = {{ database.connection }}
+{% endif -%}
 db_auto_create = false
 
 {% include "parts/section-identity" %}
 
+[keystone_auth]
+{% include "parts/identity-data" %}
+
 {% include "parts/section-service-user" %}
 
 {% include "parts/section-trust" %}
diff --git a/charms/magnum-k8s/src/templates/parts/database-connection b/charms/magnum-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 058d99ad..00000000
--- a/charms/magnum-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-sql_connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/magnum-k8s/src/templates/parts/identity-data b/charms/magnum-k8s/src/templates/parts/identity-data
deleted file mode 100644
index db28064a..00000000
--- a/charms/magnum-k8s/src/templates/parts/identity-data
+++ /dev/null
@@ -1,26 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
-
-# XXX Region should come from the id relation here
-region_name = {{ options.region }}
diff --git a/charms/magnum-k8s/src/templates/parts/section-database b/charms/magnum-k8s/src/templates/parts/section-database
deleted file mode 100644
index 986d9b10..00000000
--- a/charms/magnum-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,3 +0,0 @@
-[database]
-{% include "parts/database-connection" %}
-connection_recycle_time = 200
diff --git a/charms/magnum-k8s/src/templates/parts/section-identity b/charms/magnum-k8s/src/templates/parts/section-identity
deleted file mode 100644
index b45ab19a..00000000
--- a/charms/magnum-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,5 +0,0 @@
-[keystone_authtoken]
-{% include "parts/identity-data" %}
-
-[keystone_auth]
-{% include "parts/identity-data" %}
diff --git a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/magnum-k8s/src/templates/parts/section-service-user b/charms/magnum-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/magnum-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/magnum-k8s/test-requirements.txt b/charms/magnum-k8s/test-requirements.txt
deleted file mode 100644
index 23e005bd..00000000
--- a/charms/magnum-k8s/test-requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-pwgen
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/magnum-k8s/tests/unit/test_magnum_charm.py b/charms/magnum-k8s/tests/unit/test_magnum_charm.py
index 6466f9a4..c157e546 100644
--- a/charms/magnum-k8s/tests/unit/test_magnum_charm.py
+++ b/charms/magnum-k8s/tests/unit/test_magnum_charm.py
@@ -18,6 +18,7 @@
 
 import json
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from mock import (
     Mock,
@@ -26,8 +27,6 @@ from ops.testing import (
     Harness,
 )
 
-import charm
-
 
 class _MagnumTestOperatorCharm(charm.MagnumOperatorCharm):
     """Test Operator Charm for Magnum Operator."""
diff --git a/charms/magnum-k8s/tox.ini b/charms/magnum-k8s/tox.ini
deleted file mode 100644
index 57750258..00000000
--- a/charms/magnum-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-  HOME
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series=
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/neutron-k8s/.flake8 b/charms/neutron-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/neutron-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/neutron-k8s/.gitignore b/charms/neutron-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/neutron-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/neutron-k8s/.gitreview b/charms/neutron-k8s/.gitreview
deleted file mode 100644
index bd60210f..00000000
--- a/charms/neutron-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-neutron-k8s.git
-defaultbranch=main
diff --git a/charms/neutron-k8s/.jujuignore b/charms/neutron-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/neutron-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/neutron-k8s/.stestr.conf b/charms/neutron-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/neutron-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/neutron-k8s/.zuul.yaml b/charms/neutron-k8s/.zuul.yaml
deleted file mode 100644
index 710da9f9..00000000
--- a/charms/neutron-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: neutron-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/neutron-k8s/charmcraft.yaml b/charms/neutron-k8s/charmcraft.yaml
index 5ac17253..fa60d165 100644
--- a/charms/neutron-k8s/charmcraft.yaml
+++ b/charms/neutron-k8s/charmcraft.yaml
@@ -29,4 +29,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/neutron-k8s/fetch-libs.sh b/charms/neutron-k8s/fetch-libs.sh
deleted file mode 100755
index 4f3bd45d..00000000
--- a/charms/neutron-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
-charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
diff --git a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/neutron-k8s/osci.yaml b/charms/neutron-k8s/osci.yaml
deleted file mode 100644
index dba34983..00000000
--- a/charms/neutron-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: neutron-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/neutron-k8s/pyproject.toml b/charms/neutron-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/neutron-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/neutron-k8s/rename.sh b/charms/neutron-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/neutron-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/neutron-k8s/requirements.txt b/charms/neutron-k8s/requirements.txt
index e988c61d..64399ba9 100644
--- a/charms/neutron-k8s/requirements.txt
+++ b/charms/neutron-k8s/requirements.txt
@@ -12,4 +12,5 @@ lightkube
 lightkube-models
 ops
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+# From ops_sunbeam
+tenacity
diff --git a/charms/neutron-k8s/src/templates/parts/section-database b/charms/neutron-k8s/src/templates/parts/section-database
deleted file mode 100644
index eb52f65e..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/cinder/cinder.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/neutron-k8s/src/templates/parts/section-federation b/charms/neutron-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/neutron-k8s/src/templates/parts/section-identity b/charms/neutron-k8s/src/templates/parts/section-identity
deleted file mode 100644
index cbb1d069..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,24 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/neutron-k8s/src/templates/parts/section-middleware b/charms/neutron-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/neutron-k8s/src/templates/parts/section-service-user b/charms/neutron-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/neutron-k8s/src/templates/parts/section-signing b/charms/neutron-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/neutron-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/neutron-k8s/test-requirements.txt b/charms/neutron-k8s/test-requirements.txt
deleted file mode 100644
index c533b127..00000000
--- a/charms/neutron-k8s/test-requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/neutron-k8s/tests/unit/test_neutron_charm.py b/charms/neutron-k8s/tests/unit/test_neutron_charm.py
index 606d0f09..ee850769 100644
--- a/charms/neutron-k8s/tests/unit/test_neutron_charm.py
+++ b/charms/neutron-k8s/tests/unit/test_neutron_charm.py
@@ -16,9 +16,8 @@
 
 """Tests for neutron charm."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _NeutronOVNOperatorCharm(charm.NeutronOVNOperatorCharm):
diff --git a/charms/neutron-k8s/tox.ini b/charms/neutron-k8s/tox.ini
deleted file mode 100644
index fbaa02c5..00000000
--- a/charms/neutron-k8s/tox.ini
+++ /dev/null
@@ -1,169 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/nova-k8s/.flake8 b/charms/nova-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/nova-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/nova-k8s/.gitignore b/charms/nova-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/nova-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/nova-k8s/.gitreview b/charms/nova-k8s/.gitreview
deleted file mode 100644
index 1f46f0d9..00000000
--- a/charms/nova-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-nova-k8s.git
-defaultbranch=main
diff --git a/charms/nova-k8s/.jujuignore b/charms/nova-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/nova-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/nova-k8s/.stestr.conf b/charms/nova-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/nova-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/nova-k8s/.zuul.yaml b/charms/nova-k8s/.zuul.yaml
deleted file mode 100644
index 619607cb..00000000
--- a/charms/nova-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: nova-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/nova-k8s/charmcraft.yaml b/charms/nova-k8s/charmcraft.yaml
index 0c6b3bdb..cbd210b0 100644
--- a/charms/nova-k8s/charmcraft.yaml
+++ b/charms/nova-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/nova-k8s/fetch-libs.sh b/charms/nova-k8s/fetch-libs.sh
deleted file mode 100755
index 896bde54..00000000
--- a/charms/nova-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
-charmcraft fetch-lib charms.nova_compute_k8s.v0.cloud_compute
diff --git a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/nova-k8s/osci.yaml b/charms/nova-k8s/osci.yaml
deleted file mode 100644
index d707a693..00000000
--- a/charms/nova-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: nova-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/nova-k8s/pyproject.toml b/charms/nova-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/nova-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/nova-k8s/rename.sh b/charms/nova-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/nova-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/nova-k8s/requirements.txt b/charms/nova-k8s/requirements.txt
index 408e0a2d..efcc785e 100644
--- a/charms/nova-k8s/requirements.txt
+++ b/charms/nova-k8s/requirements.txt
@@ -12,4 +12,6 @@ lightkube
 lightkube-models
 
 ops
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/nova-k8s/src/templates/nova.conf.j2 b/charms/nova-k8s/src/templates/nova.conf.j2
index 8f49f109..a42983fa 100644
--- a/charms/nova-k8s/src/templates/nova.conf.j2
+++ b/charms/nova-k8s/src/templates/nova.conf.j2
@@ -13,12 +13,7 @@ connection = sqlite:////var/lib/nova/nova_api.sqlite
 {% endif -%}
 connection_recycle_time = 200
 
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/nova/nova.sqlite
-{% endif -%}
+{% include "parts/section-database" %}
 
 [glance]
 service_type = image
@@ -26,14 +21,16 @@ service_name = glance
 valid_interfaces = admin
 region_name = {{ options.region }}
 
-[keystone_authtoken]
 {% include "parts/section-identity" %}
+region_name = {{ options.region }}
 
 [neutron]
-{% include "parts/section-identity" %}
+{% include "parts/identity-data" %}
+region_name = {{ options.region }}
 
 [placement]
-{% include "parts/section-identity" %}
+{% include "parts/identity-data" %}
+region_name = {{ options.region }}
 
 {% include "parts/section-service-user" %}
 
diff --git a/charms/nova-k8s/src/templates/parts/section-identity b/charms/nova-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 5a48d675..00000000
--- a/charms/nova-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,25 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-# XXX Region should come from the id relation here
-region_name = {{ options.region }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit
deleted file mode 100644
index 145c4ee9..00000000
--- a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit
+++ /dev/null
@@ -1,2 +0,0 @@
-[oslo_messaging_rabbit]
-rabbit_quorum_queue = True
diff --git a/charms/nova-k8s/src/templates/parts/section-service-user b/charms/nova-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/nova-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/nova-k8s/test-requirements.txt b/charms/nova-k8s/test-requirements.txt
deleted file mode 100644
index a9b0d698..00000000
--- a/charms/nova-k8s/test-requirements.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/nova-k8s/tests/unit/test_nova_charm.py b/charms/nova-k8s/tests/unit/test_nova_charm.py
index c9adf611..a9285295 100644
--- a/charms/nova-k8s/tests/unit/test_nova_charm.py
+++ b/charms/nova-k8s/tests/unit/test_nova_charm.py
@@ -16,9 +16,8 @@
 
 """Unit tests for Nova operator."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _NovaTestOperatorCharm(charm.NovaOperatorCharm):
diff --git a/charms/nova-k8s/tox.ini b/charms/nova-k8s/tox.ini
deleted file mode 100644
index f6fb644e..00000000
--- a/charms/nova-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true;update-status-hook-interval=1m
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/octavia-k8s/.gitignore b/charms/octavia-k8s/.gitignore
deleted file mode 100644
index 24ff2e41..00000000
--- a/charms/octavia-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
diff --git a/charms/octavia-k8s/.gitreview b/charms/octavia-k8s/.gitreview
deleted file mode 100644
index 26336e73..00000000
--- a/charms/octavia-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-octavia-k8s.git
-defaultbranch=main
diff --git a/charms/octavia-k8s/.stestr.conf b/charms/octavia-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/octavia-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/octavia-k8s/.zuul.yaml b/charms/octavia-k8s/.zuul.yaml
deleted file mode 100644
index fc2ae5c1..00000000
--- a/charms/octavia-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: octavia-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/octavia-k8s/charmcraft.yaml b/charms/octavia-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/octavia-k8s/charmcraft.yaml
+++ b/charms/octavia-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/octavia-k8s/fetch-libs.sh b/charms/octavia-k8s/fetch-libs.sh
deleted file mode 100755
index fbd3ab38..00000000
--- a/charms/octavia-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
-charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
diff --git a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 11ffd6ca..00000000
--- a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright 2023 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.
-
-r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 6
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        if not self.relation.app:
-            return None
-
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()  # pyright: ignore [reportGeneralTypeIssues]
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: Optional[str] = None,
-        relations_aliases: Optional[List[str]] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = (
-            {key: value for key, value in event.relation.data[event.app].items() if key != "data"}
-            if event.app
-            else {}
-        )
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = (
-                {key: value for key, value in relation.data[relation.app].items() if key != "data"}
-                if relation.app
-                else {}
-            )
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            getattr(self.on, "database_created").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            getattr(self.on, "endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            getattr(self.on, "read_only_endpoints_changed").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
deleted file mode 100644
index 154fab83..00000000
--- a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
+++ /dev/null
@@ -1,392 +0,0 @@
-"""IdentityResourceProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the identity_ops interface.
-
-Import `IdentityResourceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_ops"
-
-Also provide additional parameters to the charm object:
-    - request
-
-Three events are also available to respond to:
-    - provider_ready
-    - provider_goneaway
-    - response_avaialable
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
-
-class IdentityResourceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityResource Requires
-        self.identity_resource = IdentityResourceRequires(
-            self, "identity_ops",
-        )
-        self.framework.observe(
-            self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
-        self.framework.observe(
-            self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
-        self.framework.observe(
-            self.identity_resource.on.response_available, self._on_identity_resource_response)
-
-    def _on_identity_resource_ready(self, event):
-        '''React to the IdentityResource provider_ready event.
-
-        This event happens when n IdentityResource relation is added to the
-        model. Ready to send any ops to keystone.
-        '''
-        # Ready to send any ops.
-        pass
-
-    def _on_identity_resource_response(self, event):
-        '''React to the IdentityResource response_available event.
-
-        The IdentityResource interface will provide the response for the ops sent.
-        '''
-        # Read the response for the ops sent.
-        pass
-
-    def _on_identity_resource_goneaway(self, event):
-        '''React to the IdentityResource goneaway event.
-
-        This event happens when an IdentityResource relation is removed.
-        '''
-        # IdentityResource Relation has goneaway. No ops can be sent.
-        pass
-```
-
-A sample ops request can be of format
-{
-    "id": <request id>
-    "tag": <string to identify request>
-    "ops": [
-        {
-            "name": <op name>,
-            "params": {
-                <param 1>: <value 1>,
-                <param 2>: <value 2>
-            }
-        }
-    ]
-}
-
-For any sensitive data in the ops params, the charm can create secrets and pass
-secret id instead of sensitive data as part of ops request. The charm should
-ensure to grant secret access to provider charm i.e., keystone over relation.
-The secret content should hold the sensitive data with same name as param name.
-"""
-
-import json
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import (
-    EventBase,
-    EventSource,
-    Object,
-    ObjectEvents,
-    StoredState,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b419d4d8249e423487daafc3665ed06f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-REQUEST_NOT_SENT = 1
-REQUEST_SENT = 2
-REQUEST_PROCESSED = 3
-
-
-class IdentityOpsProviderReadyEvent(RelationEvent):
-    """Has IdentityOpsProviderReady Event."""
-
-    pass
-
-
-class IdentityOpsResponseEvent(RelationEvent):
-    """Has IdentityOpsResponse Event."""
-
-    pass
-
-
-class IdentityOpsProviderGoneAwayEvent(RelationEvent):
-    """Has IdentityOpsProviderGoneAway Event."""
-
-    pass
-
-
-class IdentityResourceResponseEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    provider_ready = EventSource(IdentityOpsProviderReadyEvent)
-    response_available = EventSource(IdentityOpsResponseEvent)
-    provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
-
-
-class IdentityResourceRequires(Object):
-    """IdentityResourceRequires class."""
-
-    on = IdentityResourceResponseEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self._stored.set_default(provider_ready=False, requests=[])
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_resource_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_resource_relation_broken,
-        )
-
-    def _on_identity_resource_relation_joined(
-        self, event: RelationJoinedEvent
-    ):
-        """Handle IdentityResource joined."""
-        self._stored.provider_ready = True
-        self.on.provider_ready.emit(event.relation)
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        id_ = self.response.get("id")
-        self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
-        self.on.response_available.emit(event.relation)
-
-    def _on_identity_resource_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle IdentityResource broken."""
-        self._stored.provider_ready = False
-        self.on.provider_goneaway.emit(event.relation)
-
-    @property
-    def _identity_resource_rel(self) -> Optional[Relation]:
-        """The IdentityResource relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def response(self) -> dict:
-        """Response object from keystone."""
-        response = self.get_remote_app_data("response")
-        if not response:
-            return {}
-
-        try:
-            return json.loads(response)
-        except Exception as e:
-            logger.debug(str(e))
-
-        return {}
-
-    def save_request_in_store(
-        self, id: str, tag: str, ops: list, state: int
-    ) -> None:
-        """Save request in the store."""
-        if id is None:
-            return
-
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                if tag:
-                    request["tag"] = tag
-                if ops:
-                    request["ops"] = ops
-                request["state"] = state
-                return
-
-        # New request
-        self._stored.requests.append(
-            {"id": id, "tag": tag, "ops": ops, "state": state}
-        )
-
-    def get_request_from_store(self, id: str) -> dict:
-        """Get request from the stote."""
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                return request
-
-        return {}
-
-    def is_request_processed(self, id: str) -> bool:
-        """Check if request is processed."""
-        for request in self._stored.requests:
-            if (
-                request.get("id") == id
-                and request.get("state") == REQUEST_PROCESSED
-            ):
-                return True
-
-        return False
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._identity_resource_rel:
-            data = self._identity_resource_rel.data[
-                self._identity_resource_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    def ready(self) -> bool:
-        """Interface is ready or not.
-
-        Interface is considered ready if the op request is processed
-        and response is sent. In case of non leader unit, just consider
-        the interface is ready.
-        """
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, set the interface to ready")
-            return True
-
-        try:
-            app_data = self._identity_resource_rel.data[self.charm.app]
-            if "request" not in app_data:
-                return False
-
-            request = json.loads(app_data["request"])
-            request_id = request.get("id")
-            response_id = self.response.get("id")
-            if request_id == response_id:
-                return True
-        except Exception as e:
-            logger.debug(str(e))
-
-        return False
-
-    def request_ops(self, request: dict) -> None:
-        """Request keystone ops."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending request")
-            return
-
-        id_ = request.get("id")
-        tag = request.get("tag")
-        ops = request.get("ops")
-        req = self.get_request_from_store(id_)
-        if req and req.get("state") == REQUEST_PROCESSED:
-            logger.debug("Request {id_} already processed")
-            return
-
-        if not self._stored.provider_ready:
-            self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
-            logger.debug("Keystone not yet ready to take requests")
-            return
-
-        logger.debug("Requesting ops to keystone")
-        app_data = self._identity_resource_rel.data[self.charm.app]
-        app_data["request"] = json.dumps(request)
-        self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
-
-
-class IdentityOpsRequestEvent(EventBase):
-    """Has IdentityOpsRequest Event."""
-
-    def __init__(self, handle, relation_id, relation_name, request):
-        """Initialise event."""
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.request = request
-
-    def snapshot(self):
-        """Snapshot the event."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "request": self.request,
-        }
-
-    def restore(self, snapshot):
-        """Restore the event."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.request = snapshot["request"]
-
-
-class IdentityResourceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    process_op = EventSource(IdentityOpsRequestEvent)
-
-
-class IdentityResourceProvides(Object):
-    """IdentityResourceProvides class."""
-
-    on = IdentityResourceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        request = event.relation.data[event.relation.app].get("request", {})
-        self.on.process_op.emit(
-            event.relation.id, event.relation.name, request
-        )
-
-    def set_ops_response(
-        self, relation_id: str, relation_name: str, ops_response: dict
-    ) -> None:
-        """Set response to ops request."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending response")
-            return
-
-        logger.debug("Update response from keystone")
-        _identity_resource_rel = self.charm.model.get_relation(
-            relation_name, relation_id
-        )
-        if not _identity_resource_rel:
-            # Relation has disappeared so skip send of data
-            return
-
-        app_data = _identity_resource_rel.data[self.charm.app]
-        app_data["response"] = json.dumps(ops_response)
diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
deleted file mode 100644
index 732679a6..00000000
--- a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""TODO: Add a proper docstring here.
-
-This is a placeholder docstring for this charm library. Docstrings are
-presented on Charmhub and updated whenever you push a new version of the
-library.
-
-Complete documentation about creating and documenting libraries can be found
-in the SDK docs at https://juju.is/docs/sdk/libraries.
-
-See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
-share and consume charm libraries. They serve to enhance collaboration
-between charmers. Use a charmer's libraries for classes that handle
-integration with their charm.
-
-Bear in mind that new revisions of the different major API versions (v0, v1,
-v2 etc) are maintained independently.  You can continue to update v0 and v1
-after you have pushed v3.
-
-Markdown is supported, following the CommonMark specification.
-"""
-
-import logging
-import typing
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "114b7bb1970445daa61650e451f9da62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-# TODO: add your code here! Happy coding!
-class OVSDBCMSConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSConnectedEvent)
-    ready = EventSource(OVSDBCMSReadyEvent)
-    goneaway = EventSource(OVSDBCMSGoneAwayEvent)
-
-
-class OVSDBCMSRequires(Object):
-    """
-    OVSDBCMSRequires class
-    """
-
-    on = OVSDBCMSServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """OVSDBCMS relation joined."""
-        logging.debug("OVSDBCMSRequires on_joined")
-        self.on.connected.emit()
-
-    def bound_hostnames(self):
-        return self.get_all_unit_values("bound-hostname")
-
-    def bound_addresses(self):
-        return self.get_all_unit_values("bound-address")
-
-    def remote_ready(self):
-        return all(self.bound_hostnames()) or all(self.bound_addresses())
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """OVSDBCMS relation changed."""
-        logging.debug("OVSDBCMSRequires on_changed")
-        if self.remote_ready():
-            self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """OVSDBCMS relation broken."""
-        logging.debug("OVSDBCMSRequires on_broken")
-        self.on.goneaway.emit()
-
-    def get_all_unit_values(self, key: str) -> typing.List[str]:
-        """Retrieve value for key from all related units."""
-        values = []
-        relation = self.framework.model.get_relation(self.relation_name)
-        if relation:
-            for unit in relation.units:
-                values.append(relation.data[unit].get(key))
-        return values
-
-
-
-class OVSDBCMSClientConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSClientReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSClientGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSClientConnectedEvent)
-    ready = EventSource(OVSDBCMSClientReadyEvent)
-    goneaway = EventSource(OVSDBCMSClientGoneAwayEvent)
-
-
-class OVSDBCMSProvides(Object):
-    """
-    OVSDBCMSProvides class
-    """
-
-    on = OVSDBCMSClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """Handle ovsdb-cms joined."""
-        logging.debug("OVSDBCMSProvides on_joined")
-        self.on.connected.emit()
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """Handle ovsdb-cms changed."""
-        logging.debug("OVSDBCMSProvides on_changed")
-        self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """Handle ovsdb-cms broken."""
-        logging.debug("OVSDBCMSProvides on_departed")
-        self.on.goneaway.emit()
-
-    def set_unit_data(self, settings: typing.Dict[str, str]) -> None:
-        """Publish settings on the peer unit data bag."""
-        relations = self.framework.model.relations[self.relation_name]
-        for relation in relations:
-            for k, v in settings.items():
-                relation.data[self.model.unit][k] = v
diff --git a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
deleted file mode 100644
index be171d8e..00000000
--- a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
+++ /dev/null
@@ -1,1360 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-
-"""Library for the tls-certificates relation.
-
-This library contains the Requires and Provides classes for handling the tls-certificates
-interface.
-
-## Getting Started
-From a charm directory, fetch the library using `charmcraft`:
-
-```shell
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-```
-
-Add the following libraries to the charm's `requirements.txt` file:
-- jsonschema
-- cryptography
-
-Add the following section to the charm's `charmcraft.yaml` file:
-```yaml
-parts:
-  charm:
-    build-packages:
-      - libffi-dev
-      - libssl-dev
-      - rustc
-      - cargo
-```
-
-### Provider charm
-The provider charm is the charm providing certificates to another charm that requires them. In
-this example, the provider charm is storing its private key using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateCreationRequestEvent,
-    CertificateRevocationRequestEvent,
-    TLSCertificatesProvidesV1,
-    generate_private_key,
-)
-from ops.charm import CharmBase, InstallEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-def generate_ca(private_key: bytes, subject: str) -> str:
-    return "whatever ca content"
-
-
-def generate_certificate(ca: str, private_key: str, csr: str) -> str:
-    return "Whatever certificate"
-
-
-class ExampleProviderCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.certificates = TLSCertificatesProvidesV1(self, "certificates")
-        self.framework.observe(
-            self.certificates.on.certificate_request, self._on_certificate_request
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revocation_request
-        )
-        self.framework.observe(self.on.install, self._on_install)
-
-    def _on_install(self, event: InstallEvent) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        ca_certificate = generate_ca(private_key=private_key, subject="whatever")
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {
-                "private_key_password": "banana",
-                "private_key": private_key,
-                "ca_certificate": ca_certificate,
-            }
-        )
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        certificate = generate_certificate(
-            ca=ca_certificate,
-            private_key=private_key,
-            csr=event.certificate_signing_request,
-        )
-
-        self.certificates.set_relation_certificate(
-            certificate=certificate,
-            certificate_signing_request=event.certificate_signing_request,
-            ca=ca_certificate,
-            chain=[ca_certificate, certificate],
-            relation_id=event.relation_id,
-        )
-
-    def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
-        # Do what you want to do with this information
-        pass
-
-
-if __name__ == "__main__":
-    main(ExampleProviderCharm)
-```
-
-### Requirer charm
-The requirer charm is the charm requiring certificates from another charm that provides them. In
-this example, the requirer charm is storing its certificates using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateAvailableEvent,
-    CertificateExpiringEvent,
-    CertificateRevokedEvent,
-    TLSCertificatesRequiresV1,
-    generate_csr,
-    generate_private_key,
-)
-from ops.charm import CharmBase, RelationJoinedEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-class ExampleRequirerCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.cert_subject = "whatever"
-        self.certificates = TLSCertificatesRequiresV1(self, "certificates")
-        self.framework.observe(self.on.install, self._on_install)
-        self.framework.observe(
-            self.on.certificates_relation_joined, self._on_certificates_relation_joined
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_available, self._on_certificate_available
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_expiring, self._on_certificate_expiring
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revoked
-        )
-
-    def _on_install(self, event) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {"private_key_password": "banana", "private_key": private_key.decode()}
-        )
-
-    def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        replicas_relation.data[self.app].update({"csr": csr.decode()})
-        self.certificates.request_certificate_creation(certificate_signing_request=csr)
-
-    def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update({"certificate": event.certificate})
-        replicas_relation.data[self.app].update({"ca": event.ca})
-        replicas_relation.data[self.app].update({"chain": event.chain})
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-
-    def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-        replicas_relation.data[self.app].pop("certificate")
-        replicas_relation.data[self.app].pop("ca")
-        replicas_relation.data[self.app].pop("chain")
-        self.unit.status = WaitingStatus("Waiting for new certificate")
-
-
-if __name__ == "__main__":
-    main(ExampleRequirerCharm)
-```
-"""  # noqa: D405, D410, D411, D214, D416
-
-import copy
-import json
-import logging
-import uuid
-from datetime import datetime, timedelta
-from ipaddress import IPv4Address
-from typing import Dict, List, Optional
-
-from cryptography import x509
-from cryptography.hazmat._oid import ExtensionOID
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.x509.extensions import Extension, ExtensionNotFound
-from jsonschema import exceptions, validate  # type: ignore[import]
-from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent
-from ops.framework import EventBase, EventSource, Handle, Object
-
-# The unique Charmhub library identifier, never change it
-LIBID = "afd8c2bccf834997afce12c2706d2ede"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 12
-
-
-REQUIRER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` requirer root schema",
-    "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificate_signing_requests": [
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-            ]
-        }
-    ],
-    "properties": {
-        "certificate_signing_requests": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {"certificate_signing_request": {"type": "string"}},
-                "required": ["certificate_signing_request"],
-            },
-        }
-    },
-    "required": ["certificate_signing_requests"],
-    "additionalProperties": True,
-}
-
-PROVIDER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` provider root schema",
-    "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                }
-            ]
-        },
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                    "revoked": True,
-                }
-            ]
-        },
-    ],
-    "properties": {
-        "certificates": {
-            "$id": "#/properties/certificates",
-            "type": "array",
-            "items": {
-                "$id": "#/properties/certificates/items",
-                "type": "object",
-                "required": ["certificate_signing_request", "certificate", "ca", "chain"],
-                "properties": {
-                    "certificate_signing_request": {
-                        "$id": "#/properties/certificates/items/certificate_signing_request",
-                        "type": "string",
-                    },
-                    "certificate": {
-                        "$id": "#/properties/certificates/items/certificate",
-                        "type": "string",
-                    },
-                    "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
-                    "chain": {
-                        "$id": "#/properties/certificates/items/chain",
-                        "type": "array",
-                        "items": {
-                            "type": "string",
-                            "$id": "#/properties/certificates/items/chain/items",
-                        },
-                    },
-                    "revoked": {
-                        "$id": "#/properties/certificates/items/revoked",
-                        "type": "boolean",
-                    },
-                },
-                "additionalProperties": True,
-            },
-        }
-    },
-    "required": ["certificates"],
-    "additionalProperties": True,
-}
-
-
-logger = logging.getLogger(__name__)
-
-
-class CertificateAvailableEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is available."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-class CertificateExpiringEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is almost expired."""
-
-    def __init__(self, handle, certificate: str, expiry: str):
-        """CertificateExpiringEvent.
-
-        Args:
-            handle (Handle): Juju framework handle
-            certificate (str): TLS Certificate
-            expiry (str): Datetime string reprensenting the time at which the certificate
-                won't be valid anymore.
-        """
-        super().__init__(handle)
-        self.certificate = certificate
-        self.expiry = expiry
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate, "expiry": self.expiry}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.expiry = snapshot["expiry"]
-
-
-class CertificateExpiredEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is expired."""
-
-    def __init__(self, handle: Handle, certificate: str):
-        super().__init__(handle)
-        self.certificate = certificate
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-
-
-class CertificateRevokedEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        revoked: bool,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-        self.revoked = revoked
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-            "revoked": self.revoked,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-        self.revoked = snapshot["revoked"]
-
-
-class CertificateCreationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is required."""
-
-    def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int):
-        super().__init__(handle)
-        self.certificate_signing_request = certificate_signing_request
-        self.relation_id = relation_id
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate_signing_request": self.certificate_signing_request,
-            "relation_id": self.relation_id,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.relation_id = snapshot["relation_id"]
-
-
-class CertificateRevocationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate needs to be revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: str,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-def _load_relation_data(raw_relation_data: dict) -> dict:
-    """Loads relation data from the relation data bag.
-
-    Json loads all data.
-
-    Args:
-        raw_relation_data: Relation data from the databag
-
-    Returns:
-        dict: Relation data in dict format.
-    """
-    certificate_data = dict()
-    for key in raw_relation_data:
-        try:
-            certificate_data[key] = json.loads(raw_relation_data[key])
-        except (json.decoder.JSONDecodeError, TypeError):
-            certificate_data[key] = raw_relation_data[key]
-    return certificate_data
-
-
-def generate_ca(
-    private_key: bytes,
-    subject: str,
-    private_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    country: str = "US",
-) -> bytes:
-    """Generates a CA Certificate.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): Certificate subject
-        private_key_password (bytes): Private key password
-        validity (int): Certificate validity time (in days)
-        country (str): Certificate Issuing country
-
-    Returns:
-        bytes: CA Certificate.
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    subject = issuer = x509.Name(
-        [
-            x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
-            x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
-        ]
-    )
-    subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
-        private_key_object.public_key()  # type: ignore[arg-type]
-    )
-    subject_identifier = key_identifier = subject_identifier_object.public_bytes()
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(private_key_object.public_key())  # type: ignore[arg-type]
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-        .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
-        .add_extension(
-            x509.AuthorityKeyIdentifier(
-                key_identifier=key_identifier,
-                authority_cert_issuer=None,
-                authority_cert_serial_number=None,
-            ),
-            critical=False,
-        )
-        .add_extension(
-            x509.BasicConstraints(ca=True, path_length=None),
-            critical=True,
-        )
-        .sign(private_key_object, hashes.SHA256())  # type: ignore[arg-type]
-    )
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_certificate(
-    csr: bytes,
-    ca: bytes,
-    ca_key: bytes,
-    ca_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    alt_names: Optional[List[str]] = None,
-) -> bytes:
-    """Generates a TLS certificate based on a CSR.
-
-    Args:
-        csr (bytes): CSR
-        ca (bytes): CA Certificate
-        ca_key (bytes): CA private key
-        ca_key_password: CA private key password
-        validity (int): Certificate validity (in days)
-        alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
-
-    Returns:
-        bytes: Certificate
-    """
-    csr_object = x509.load_pem_x509_csr(csr)
-    subject = csr_object.subject
-    issuer = x509.load_pem_x509_certificate(ca).issuer
-    private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
-
-    certificate_builder = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(csr_object.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-    )
-
-    extensions_list = csr_object.extensions
-    san_ext: Optional[x509.Extension] = None
-    if alt_names:
-        full_sans_dns = alt_names.copy()
-        try:
-            loaded_san_ext = csr_object.extensions.get_extension_for_class(
-                x509.SubjectAlternativeName
-            )
-            full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName))
-        except ExtensionNotFound:
-            pass
-        finally:
-            san_ext = Extension(
-                ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
-                False,
-                x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]),
-            )
-            if not extensions_list:
-                extensions_list = x509.Extensions([san_ext])
-
-    for extension in extensions_list:
-        if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext:
-            extension = san_ext
-
-        certificate_builder = certificate_builder.add_extension(
-            extension.value,
-            critical=extension.critical,
-        )
-    certificate_builder._version = x509.Version.v3
-    cert = certificate_builder.sign(private_key, hashes.SHA256())  # type: ignore[arg-type]
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_pfx_package(
-    certificate: bytes,
-    private_key: bytes,
-    package_password: str,
-    private_key_password: Optional[bytes] = None,
-) -> bytes:
-    """Generates a PFX package to contain the TLS certificate and private key.
-
-    Args:
-        certificate (bytes): TLS certificate
-        private_key (bytes): Private key
-        package_password (str): Password to open the PFX package
-        private_key_password (bytes): Private key password
-
-    Returns:
-        bytes:
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    certificate_object = x509.load_pem_x509_certificate(certificate)
-    name = certificate_object.subject.rfc4514_string()
-    pfx_bytes = pkcs12.serialize_key_and_certificates(
-        name=name.encode(),
-        cert=certificate_object,
-        key=private_key_object,  # type: ignore[arg-type]
-        cas=None,
-        encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()),
-    )
-    return pfx_bytes
-
-
-def generate_private_key(
-    password: Optional[bytes] = None,
-    key_size: int = 2048,
-    public_exponent: int = 65537,
-) -> bytes:
-    """Generates a private key.
-
-    Args:
-        password (bytes): Password for decrypting the private key
-        key_size (int): Key size in bytes
-        public_exponent: Public exponent.
-
-    Returns:
-        bytes: Private Key
-    """
-    private_key = rsa.generate_private_key(
-        public_exponent=public_exponent,
-        key_size=key_size,
-    )
-    key_bytes = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.TraditionalOpenSSL,
-        encryption_algorithm=serialization.BestAvailableEncryption(password)
-        if password
-        else serialization.NoEncryption(),
-    )
-    return key_bytes
-
-
-def generate_csr(
-    private_key: bytes,
-    subject: str,
-    add_unique_id_to_subject_name: bool = True,
-    organization: Optional[str] = None,
-    email_address: Optional[str] = None,
-    country_name: Optional[str] = None,
-    private_key_password: Optional[bytes] = None,
-    sans: Optional[List[str]] = None,
-    sans_oid: Optional[List[str]] = None,
-    sans_ip: Optional[List[str]] = None,
-    sans_dns: Optional[List[str]] = None,
-    additional_critical_extensions: Optional[List] = None,
-) -> bytes:
-    """Generates a CSR using private key and subject.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): CSR Subject.
-        add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
-            subject name. Always leave to "True" when the CSR is used to request certificates
-            using the tls-certificates relation.
-        organization (str): Name of organization.
-        email_address (str): Email address.
-        country_name (str): Country Name.
-        private_key_password (bytes): Private key password
-        sans (list): Use sans_dns - this will be deprecated in a future release
-            List of DNS subject alternative names (keeping it for now for backward compatibility)
-        sans_oid (list): List of registered ID SANs
-        sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
-        sans_ip (list): List of IP subject alternative names
-        additional_critical_extensions (list): List if critical additional extension objects.
-            Object must be a x509 ExtensionType.
-
-    Returns:
-        bytes: CSR
-    """
-    signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
-    subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
-    if add_unique_id_to_subject_name:
-        unique_identifier = uuid.uuid4()
-        subject_name.append(
-            x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
-        )
-    if organization:
-        subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
-    if email_address:
-        subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
-    if country_name:
-        subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
-    csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
-
-    _sans: List[x509.GeneralName] = []
-    if sans_oid:
-        _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
-    if sans_ip:
-        _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
-    if sans:
-        _sans.extend([x509.DNSName(san) for san in sans])
-    if sans_dns:
-        _sans.extend([x509.DNSName(san) for san in sans_dns])
-    if _sans:
-        csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
-
-    if additional_critical_extensions:
-        for extension in additional_critical_extensions:
-            csr = csr.add_extension(extension, critical=True)
-
-    signed_certificate = csr.sign(signing_key, hashes.SHA256())  # type: ignore[arg-type]
-    return signed_certificate.public_bytes(serialization.Encoding.PEM)
-
-
-class CertificatesProviderCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates provider charm can leverage."""
-
-    certificate_creation_request = EventSource(CertificateCreationRequestEvent)
-    certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
-
-
-class CertificatesRequirerCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates requirer charm can leverage."""
-
-    certificate_available = EventSource(CertificateAvailableEvent)
-    certificate_expiring = EventSource(CertificateExpiringEvent)
-    certificate_expired = EventSource(CertificateExpiredEvent)
-    certificate_revoked = EventSource(CertificateRevokedEvent)
-
-
-class TLSCertificatesProvidesV1(Object):
-    """TLS certificates provider class to be instantiated by TLS certificates providers."""
-
-    on = CertificatesProviderCharmEvents()
-
-    def __init__(self, charm: CharmBase, relationship_name: str):
-        super().__init__(charm, relationship_name)
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.charm = charm
-        self.relationship_name = relationship_name
-
-    def _add_certificate(
-        self,
-        relation_id: int,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ) -> None:
-        """Adds certificate to relation data.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate Signing Request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_certificate = {
-            "certificate": certificate,
-            "certificate_signing_request": certificate_signing_request,
-            "ca": ca,
-            "chain": chain,
-        }
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        if new_certificate in certificates:
-            logger.info("Certificate already in relation data - Doing nothing")
-            return
-        certificates.append(new_certificate)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    def _remove_certificate(
-        self,
-        relation_id: int,
-        certificate: Optional[str] = None,
-        certificate_signing_request: Optional[str] = None,
-    ) -> None:
-        """Removes certificate from a given relation based on user provided certificate or csr.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate (optional)
-            certificate_signing_request: Certificate signing request (optional)
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name,
-            relation_id=relation_id,
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
-            )
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        for certificate_dict in certificates:
-            if certificate and certificate_dict["certificate"] == certificate:
-                certificates.remove(certificate_dict)
-            if (
-                certificate_signing_request
-                and certificate_dict["certificate_signing_request"] == certificate_signing_request
-            ):
-                certificates.remove(certificate_dict)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Uses JSON schema validator to validate relation data content.
-
-        Args:
-            certificates_data (dict): Certificate data dictionary as retrieved from relation data.
-
-        Returns:
-            bool: True/False depending on whether the relation data follows the json schema.
-        """
-        try:
-            validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def revoke_all_certificates(self) -> None:
-        """Revokes all certificates of this provider.
-
-        This method is meant to be used when the Root CA has changed.
-        """
-        for relation in self.model.relations[self.relationship_name]:
-            provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-            provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", []))
-            for certificate in provider_certificates:
-                certificate["revoked"] = True
-            relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates)
-
-    def set_relation_certificate(
-        self,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        relation_id: int,
-    ) -> None:
-        """Adds certificates to relation data.
-
-        Args:
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate signing request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-            relation_id (int): Juju relation ID
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        self._remove_certificate(
-            certificate_signing_request=certificate_signing_request.strip(),
-            relation_id=relation_id,
-        )
-        self._add_certificate(
-            relation_id=relation_id,
-            certificate=certificate.strip(),
-            certificate_signing_request=certificate_signing_request.strip(),
-            ca=ca.strip(),
-            chain=[cert.strip() for cert in chain],
-        )
-
-    def remove_certificate(self, certificate: str) -> None:
-        """Removes a given certificate from relation data.
-
-        Args:
-            certificate (str): TLS Certificate
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.relations[self.relationship_name]
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        for certificate_relation in certificates_relation:
-            self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed event.
-
-        Looks at the relation data and either emits:
-        - certificate request event: If the unit relation data contains a CSR for which
-            a certificate does not exist in the provider relation data.
-        - certificate revocation event: If the provider relation data contains a CSR for which
-            a csr does not exist in the requirer relation data.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        assert event.unit is not None
-        requirer_relation_data = _load_relation_data(event.relation.data[event.unit])
-        provider_relation_data = _load_relation_data(event.relation.data[self.charm.app])
-        if not self._relation_data_is_valid(requirer_relation_data):
-            logger.warning(
-                f"Relation data did not pass JSON Schema validation: {requirer_relation_data}"
-            )
-            return
-        provider_certificates = provider_relation_data.get("certificates", [])
-        requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-        provider_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in provider_certificates
-        ]
-        requirer_unit_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in requirer_csrs
-        ]
-        for certificate_signing_request in requirer_unit_csrs:
-            if certificate_signing_request not in provider_csrs:
-                self.on.certificate_creation_request.emit(
-                    certificate_signing_request=certificate_signing_request,
-                    relation_id=event.relation.id,
-                )
-        self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
-
-    def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
-        """Revokes certificates for which no unit has a CSR.
-
-        Goes through all generated certificates and compare agains the list of CSRS for all units
-        of a given relationship.
-
-        Args:
-            relation_id (int): Relation id
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app])
-        list_of_csrs: List[str] = []
-        for unit in certificates_relation.units:
-            requirer_relation_data = _load_relation_data(certificates_relation.data[unit])
-            requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-            list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs)
-        provider_certificates = provider_relation_data.get("certificates", [])
-        for certificate in provider_certificates:
-            if certificate["certificate_signing_request"] not in list_of_csrs:
-                self.on.certificate_revocation_request.emit(
-                    certificate=certificate["certificate"],
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-                self.remove_certificate(certificate=certificate["certificate"])
-
-
-class TLSCertificatesRequiresV1(Object):
-    """TLS certificates requirer class to be instantiated by TLS certificates requirers."""
-
-    on = CertificatesRequirerCharmEvents()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relationship_name: str,
-        expiry_notification_time: int = 168,
-    ):
-        """Generates/use private key and observes relation changed event.
-
-        Args:
-            charm: Charm object
-            relationship_name: Juju relation name
-            expiry_notification_time (int): Time difference between now and expiry (in hours).
-                Used to trigger the CertificateExpiring event. Default: 7 days.
-        """
-        super().__init__(charm, relationship_name)
-        self.relationship_name = relationship_name
-        self.charm = charm
-        self.expiry_notification_time = expiry_notification_time
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(charm.on.update_status, self._on_update_status)
-
-    @property
-    def _requirer_csrs(self) -> List[Dict[str, str]]:
-        """Returns list of requirer CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
-        return requirer_relation_data.get("certificate_signing_requests", [])
-
-    @property
-    def _provider_certificates(self) -> List[Dict[str, str]]:
-        """Returns list of provider CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        if not relation.app:
-            raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        return provider_relation_data.get("certificates", [])
-
-    def _add_requirer_csr(self, csr: str) -> None:
-        """Adds CSR to relation data.
-
-        Args:
-            csr (str): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_csr_dict = {"certificate_signing_request": csr}
-        if new_csr_dict in self._requirer_csrs:
-            logger.info("CSR already in relation data - Doing nothing")
-            return
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        requirer_csrs.append(new_csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def _remove_requirer_csr(self, csr: str) -> None:
-        """Removes CSR from relation data.
-
-        Args:
-            csr (str): Certificate signing request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        csr_dict = {"certificate_signing_request": csr}
-        if csr_dict not in requirer_csrs:
-            logger.info("CSR not in relation data - Doing nothing")
-            return
-        requirer_csrs.remove(csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def request_certificate_creation(self, certificate_signing_request: bytes) -> None:
-        """Request TLS certificate to provider charm.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            message = (
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-            logger.error(message)
-            raise RuntimeError(message)
-        self._add_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate request sent to provider")
-
-    def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
-        """Removes CSR from relation data.
-
-        The provider of this relation is then expected to remove certificates associated to this
-        CSR from the relation data as well and emit a request_certificate_revocation event for the
-        provider charm to interpret.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        self._remove_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate revocation sent to provider")
-
-    def request_certificate_renewal(
-        self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
-    ) -> None:
-        """Renews certificate.
-
-        Removes old CSR from relation data and adds new one.
-
-        Args:
-            old_certificate_signing_request: Old CSR
-            new_certificate_signing_request: New CSR
-
-        Returns:
-            None
-        """
-        try:
-            self.request_certificate_revocation(
-                certificate_signing_request=old_certificate_signing_request
-            )
-        except RuntimeError:
-            logger.warning("Certificate revocation failed.")
-        self.request_certificate_creation(
-            certificate_signing_request=new_certificate_signing_request
-        )
-        logger.info("Certificate renewal request completed.")
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Checks whether relation data is valid based on json schema.
-
-        Args:
-            certificates_data: Certificate data in dict format.
-
-        Returns:
-            bool: Whether relation data is valid.
-        """
-        try:
-            validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggered on relation changed events.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{event.relation.data[relation.app]}"
-            )
-            return
-        requirer_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in self._requirer_csrs
-        ]
-        for certificate in self._provider_certificates:
-            if certificate["certificate_signing_request"] in requirer_csrs:
-                if certificate.get("revoked", False):
-                    self.on.certificate_revoked.emit(
-                        certificate_signing_request=certificate["certificate_signing_request"],
-                        certificate=certificate["certificate"],
-                        ca=certificate["ca"],
-                        chain=certificate["chain"],
-                        revoked=True,
-                    )
-                else:
-                    self.on.certificate_available.emit(
-                        certificate_signing_request=certificate["certificate_signing_request"],
-                        certificate=certificate["certificate"],
-                        ca=certificate["ca"],
-                        chain=certificate["chain"],
-                    )
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Triggered on update status event.
-
-        Goes through each certificate in the "certificates" relation and checks their expiry date.
-        If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if
-        they are expired, emits a CertificateExpiredEvent.
-
-        Args:
-            event (UpdateStatusEvent): Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{relation.data[relation.app]}"
-            )
-            return
-        for certificate_dict in self._provider_certificates:
-            certificate = certificate_dict["certificate"]
-            try:
-                certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
-            except ValueError:
-                logger.warning("Could not load certificate.")
-                continue
-            time_difference = certificate_object.not_valid_after - datetime.utcnow()
-            if time_difference.total_seconds() < 0:
-                logger.warning("Certificate is expired")
-                self.on.certificate_expired.emit(certificate=certificate)
-                self.request_certificate_revocation(certificate.encode())
-                continue
-            if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60):
-                logger.warning("Certificate almost expired")
-                self.on.certificate_expiring.emit(
-                    certificate=certificate, expiry=certificate_object.not_valid_after.isoformat()
-                )
diff --git a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/octavia-k8s/osci.yaml b/charms/octavia-k8s/osci.yaml
deleted file mode 100644
index 0cb2ddd6..00000000
--- a/charms/octavia-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: octavia-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/octavia-k8s/pyproject.toml b/charms/octavia-k8s/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/octavia-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/octavia-k8s/rename.sh b/charms/octavia-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/octavia-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/octavia-k8s/requirements.txt b/charms/octavia-k8s/requirements.txt
index 230257ac..db47310f 100644
--- a/charms/octavia-k8s/requirements.txt
+++ b/charms/octavia-k8s/requirements.txt
@@ -3,5 +3,7 @@ ops
 jinja2
 jsonschema
 pydantic<2.0
-git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
 lightkube
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/octavia-k8s/src/templates/parts/database-connection b/charms/octavia-k8s/src/templates/parts/database-connection
deleted file mode 100644
index 1fd70ce2..00000000
--- a/charms/octavia-k8s/src/templates/parts/database-connection
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% endif -%}
diff --git a/charms/octavia-k8s/src/templates/parts/identity-data b/charms/octavia-k8s/src/templates/parts/identity-data
deleted file mode 100644
index 574c3248..00000000
--- a/charms/octavia-k8s/src/templates/parts/identity-data
+++ /dev/null
@@ -1,21 +0,0 @@
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
diff --git a/charms/octavia-k8s/src/templates/parts/section-database b/charms/octavia-k8s/src/templates/parts/section-database
deleted file mode 100644
index 986d9b10..00000000
--- a/charms/octavia-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,3 +0,0 @@
-[database]
-{% include "parts/database-connection" %}
-connection_recycle_time = 200
diff --git a/charms/octavia-k8s/src/templates/parts/section-federation b/charms/octavia-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/octavia-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/octavia-k8s/src/templates/parts/section-identity b/charms/octavia-k8s/src/templates/parts/section-identity
deleted file mode 100644
index 2cf792eb..00000000
--- a/charms/octavia-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,3 +0,0 @@
-[keystone_authtoken]
-{% include "parts/identity-data" %}
-{% include "parts/service-token" %}
diff --git a/charms/octavia-k8s/src/templates/parts/section-middleware b/charms/octavia-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/octavia-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/octavia-k8s/src/templates/parts/section-signing b/charms/octavia-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/octavia-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/octavia-k8s/src/templates/parts/service-token b/charms/octavia-k8s/src/templates/parts/service-token
deleted file mode 100644
index 51c0073f..00000000
--- a/charms/octavia-k8s/src/templates/parts/service-token
+++ /dev/null
@@ -1,2 +0,0 @@
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/octavia-k8s/test-requirements.txt b/charms/octavia-k8s/test-requirements.txt
deleted file mode 100644
index d1a61d34..00000000
--- a/charms/octavia-k8s/test-requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
diff --git a/charms/octavia-k8s/tests/unit/test_charm.py b/charms/octavia-k8s/tests/unit/test_charm.py
index e84092d4..a4b7f6e1 100644
--- a/charms/octavia-k8s/tests/unit/test_charm.py
+++ b/charms/octavia-k8s/tests/unit/test_charm.py
@@ -16,9 +16,8 @@
 
 """Unit tests for Octavia operator."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _OctaviaOVNOperatorCharm(charm.OctaviaOVNOperatorCharm):
diff --git a/charms/octavia-k8s/tox.ini b/charms/octavia-k8s/tox.ini
deleted file mode 100644
index 067963f9..00000000
--- a/charms/octavia-k8s/tox.ini
+++ /dev/null
@@ -1,165 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/openstack-exporter-k8s/.gitignore b/charms/openstack-exporter-k8s/.gitignore
deleted file mode 100644
index 24ff2e41..00000000
--- a/charms/openstack-exporter-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-*.swp
-.stestr/
diff --git a/charms/openstack-exporter-k8s/.gitreview b/charms/openstack-exporter-k8s/.gitreview
deleted file mode 100644
index 9859257a..00000000
--- a/charms/openstack-exporter-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-openstack-exporter-k8s.git
-defaultbranch=main
diff --git a/charms/openstack-exporter-k8s/.stestr.conf b/charms/openstack-exporter-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/openstack-exporter-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/openstack-exporter-k8s/.zuul.yaml b/charms/openstack-exporter-k8s/.zuul.yaml
deleted file mode 100644
index 5748d306..00000000
--- a/charms/openstack-exporter-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: openstack-exporter-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/openstack-exporter-k8s/charmcraft.yaml b/charms/openstack-exporter-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/openstack-exporter-k8s/charmcraft.yaml
+++ b/charms/openstack-exporter-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
deleted file mode 100644
index 1f10383a..00000000
--- a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
+++ /dev/null
@@ -1,393 +0,0 @@
-"""IdentityResourceProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the identity_ops interface.
-
-Import `IdentityResourceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_ops"
-
-Also provide additional parameters to the charm object:
-    - request
-
-Three events are also available to respond to:
-    - provider_ready
-    - provider_goneaway
-    - response_avaialable
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
-
-class IdentityResourceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityResource Requires
-        self.identity_resource = IdentityResourceRequires(
-            self, "identity_ops",
-        )
-        self.framework.observe(
-            self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
-        self.framework.observe(
-            self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
-        self.framework.observe(
-            self.identity_resource.on.response_available, self._on_identity_resource_response)
-
-    def _on_identity_resource_ready(self, event):
-        '''React to the IdentityResource provider_ready event.
-
-        This event happens when n IdentityResource relation is added to the
-        model. Ready to send any ops to keystone.
-        '''
-        # Ready to send any ops.
-        pass
-
-    def _on_identity_resource_response(self, event):
-        '''React to the IdentityResource response_available event.
-
-        The IdentityResource interface will provide the response for the ops sent.
-        '''
-        # Read the response for the ops sent.
-        pass
-
-    def _on_identity_resource_goneaway(self, event):
-        '''React to the IdentityResource goneaway event.
-
-        This event happens when an IdentityResource relation is removed.
-        '''
-        # IdentityResource Relation has goneaway. No ops can be sent.
-        pass
-```
-
-A sample ops request can be of format
-{
-    "id": <request id>
-    "tag": <string to identify request>
-    "ops": [
-        {
-            "name": <op name>,
-            "params": {
-                <param 1>: <value 1>,
-                <param 2>: <value 2>
-            }
-        }
-    ]
-}
-
-For any sensitive data in the ops params, the charm can create secrets and pass
-secret id instead of sensitive data as part of ops request. The charm should
-ensure to grant secret access to provider charm i.e., keystone over relation.
-The secret content should hold the sensitive data with same name as param name.
-"""
-
-import json
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import (
-    EventBase,
-    EventSource,
-    Object,
-    ObjectEvents,
-    StoredState,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b419d4d8249e423487daafc3665ed06f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 4
-
-
-REQUEST_NOT_SENT = 1
-REQUEST_SENT = 2
-REQUEST_PROCESSED = 3
-
-
-class IdentityOpsProviderReadyEvent(RelationEvent):
-    """Has IdentityOpsProviderReady Event."""
-
-    pass
-
-
-class IdentityOpsResponseEvent(RelationEvent):
-    """Has IdentityOpsResponse Event."""
-
-    pass
-
-
-class IdentityOpsProviderGoneAwayEvent(RelationEvent):
-    """Has IdentityOpsProviderGoneAway Event."""
-
-    pass
-
-
-class IdentityResourceResponseEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    provider_ready = EventSource(IdentityOpsProviderReadyEvent)
-    response_available = EventSource(IdentityOpsResponseEvent)
-    provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
-
-
-class IdentityResourceRequires(Object):
-    """IdentityResourceRequires class."""
-
-    on = IdentityResourceResponseEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self._stored.set_default(provider_ready=False, requests=[])
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_resource_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_resource_relation_broken,
-        )
-
-    def _on_identity_resource_relation_joined(
-        self, event: RelationJoinedEvent
-    ):
-        """Handle IdentityResource joined."""
-        self._stored.provider_ready = True
-        self.on.provider_ready.emit(event.relation)
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        id_ = self.response.get("id")
-        self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
-        self.on.response_available.emit(event.relation)
-
-    def _on_identity_resource_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle IdentityResource broken."""
-        self._stored.provider_ready = False
-        self.on.provider_goneaway.emit(event.relation)
-
-    @property
-    def _identity_resource_rel(self) -> Optional[Relation]:
-        """The IdentityResource relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def response(self) -> dict:
-        """Response object from keystone."""
-        response = self.get_remote_app_data("response")
-        if not response:
-            return {}
-
-        try:
-            return json.loads(response)
-        except Exception as e:
-            logger.debug(str(e))
-
-        return {}
-
-    def save_request_in_store(
-        self, id: str, tag: str, ops: list, state: int
-    ) -> None:
-        """Save request in the store."""
-        if id is None:
-            return
-
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                if tag:
-                    request["tag"] = tag
-                if ops:
-                    request["ops"] = ops
-                request["state"] = state
-                return
-
-        # New request
-        self._stored.requests.append(
-            {"id": id, "tag": tag, "ops": ops, "state": state}
-        )
-
-    def get_request_from_store(self, id: str) -> dict:
-        """Get request from the stote."""
-        for request in self._stored.requests:
-            if request.get("id") == id:
-                return request
-
-        return {}
-
-    def is_request_processed(self, id: str) -> bool:
-        """Check if request is processed."""
-        for request in self._stored.requests:
-            if (
-                request.get("id") == id
-                and request.get("state") == REQUEST_PROCESSED
-            ):
-                return True
-
-        return False
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._identity_resource_rel:
-            data = self._identity_resource_rel.data[
-                self._identity_resource_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    def ready(self) -> bool:
-        """Interface is ready or not.
-
-        Interface is considered ready if the op request is processed
-        and response is sent. In case of non leader unit, just consider
-        the interface is ready.
-        """
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, set the interface to ready")
-            return True
-
-        try:
-            app_data = self._identity_resource_rel.data[self.charm.app]
-            if "request" not in app_data:
-                return False
-
-            request = json.loads(app_data["request"])
-            request_id = request.get("id")
-            response_id = self.response.get("id")
-            if request_id == response_id:
-                return True
-        except Exception as e:
-            logger.debug(str(e))
-
-        return False
-
-    def request_ops(self, request: dict) -> None:
-        """Request keystone ops."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending request")
-            return
-
-        id_ = request.get("id")
-        tag = request.get("tag")
-        ops = request.get("ops")
-        req = self.get_request_from_store(id_)
-        if req and req.get("state") == REQUEST_PROCESSED:
-            logger.debug("Request {id_} already processed")
-            return
-
-        if not self._stored.provider_ready:
-            self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
-            logger.debug("Keystone not yet ready to take requests")
-            return
-
-        logger.debug("Requesting ops to keystone")
-        app_data = self._identity_resource_rel.data[self.charm.app]
-        app_data["request"] = json.dumps(request)
-        self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
-
-
-class IdentityOpsRequestEvent(EventBase):
-    """Has IdentityOpsRequest Event."""
-
-    def __init__(self, handle, relation_id, relation_name, request):
-        """Initialise event."""
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.request = request
-
-    def snapshot(self):
-        """Snapshot the event."""
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "request": self.request,
-        }
-
-    def restore(self, snapshot):
-        """Restore the event."""
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.request = snapshot["request"]
-
-
-class IdentityResourceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    process_op = EventSource(IdentityOpsRequestEvent)
-
-
-class IdentityResourceProvides(Object):
-    """IdentityResourceProvides class."""
-
-    on = IdentityResourceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_resource_relation_changed,
-        )
-
-    def _on_identity_resource_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle IdentityResource changed."""
-        request = event.relation.data[event.relation.app].get("request")
-        if request is not None:
-            self.on.process_op.emit(
-                event.relation.id, event.relation.name, request
-            )
-
-    def set_ops_response(
-        self, relation_id: str, relation_name: str, ops_response: dict
-    ) -> None:
-        """Set response to ops request."""
-        if not self.model.unit.is_leader():
-            logger.debug("Not a leader unit, not sending response")
-            return
-
-        logger.debug("Update response from keystone")
-        _identity_resource_rel = self.charm.model.get_relation(
-            relation_name, relation_id
-        )
-        if not _identity_resource_rel:
-            # Relation has disappeared so skip send of data
-            return
-
-        app_data = _identity_resource_rel.data[self.charm.app]
-        app_data["response"] = json.dumps(ops_response)
diff --git a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
deleted file mode 100644
index be171d8e..00000000
--- a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
+++ /dev/null
@@ -1,1360 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-
-"""Library for the tls-certificates relation.
-
-This library contains the Requires and Provides classes for handling the tls-certificates
-interface.
-
-## Getting Started
-From a charm directory, fetch the library using `charmcraft`:
-
-```shell
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-```
-
-Add the following libraries to the charm's `requirements.txt` file:
-- jsonschema
-- cryptography
-
-Add the following section to the charm's `charmcraft.yaml` file:
-```yaml
-parts:
-  charm:
-    build-packages:
-      - libffi-dev
-      - libssl-dev
-      - rustc
-      - cargo
-```
-
-### Provider charm
-The provider charm is the charm providing certificates to another charm that requires them. In
-this example, the provider charm is storing its private key using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateCreationRequestEvent,
-    CertificateRevocationRequestEvent,
-    TLSCertificatesProvidesV1,
-    generate_private_key,
-)
-from ops.charm import CharmBase, InstallEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-def generate_ca(private_key: bytes, subject: str) -> str:
-    return "whatever ca content"
-
-
-def generate_certificate(ca: str, private_key: str, csr: str) -> str:
-    return "Whatever certificate"
-
-
-class ExampleProviderCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.certificates = TLSCertificatesProvidesV1(self, "certificates")
-        self.framework.observe(
-            self.certificates.on.certificate_request, self._on_certificate_request
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revocation_request
-        )
-        self.framework.observe(self.on.install, self._on_install)
-
-    def _on_install(self, event: InstallEvent) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        ca_certificate = generate_ca(private_key=private_key, subject="whatever")
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {
-                "private_key_password": "banana",
-                "private_key": private_key,
-                "ca_certificate": ca_certificate,
-            }
-        )
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        certificate = generate_certificate(
-            ca=ca_certificate,
-            private_key=private_key,
-            csr=event.certificate_signing_request,
-        )
-
-        self.certificates.set_relation_certificate(
-            certificate=certificate,
-            certificate_signing_request=event.certificate_signing_request,
-            ca=ca_certificate,
-            chain=[ca_certificate, certificate],
-            relation_id=event.relation_id,
-        )
-
-    def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
-        # Do what you want to do with this information
-        pass
-
-
-if __name__ == "__main__":
-    main(ExampleProviderCharm)
-```
-
-### Requirer charm
-The requirer charm is the charm requiring certificates from another charm that provides them. In
-this example, the requirer charm is storing its certificates using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateAvailableEvent,
-    CertificateExpiringEvent,
-    CertificateRevokedEvent,
-    TLSCertificatesRequiresV1,
-    generate_csr,
-    generate_private_key,
-)
-from ops.charm import CharmBase, RelationJoinedEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-class ExampleRequirerCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.cert_subject = "whatever"
-        self.certificates = TLSCertificatesRequiresV1(self, "certificates")
-        self.framework.observe(self.on.install, self._on_install)
-        self.framework.observe(
-            self.on.certificates_relation_joined, self._on_certificates_relation_joined
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_available, self._on_certificate_available
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_expiring, self._on_certificate_expiring
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revoked
-        )
-
-    def _on_install(self, event) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {"private_key_password": "banana", "private_key": private_key.decode()}
-        )
-
-    def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        replicas_relation.data[self.app].update({"csr": csr.decode()})
-        self.certificates.request_certificate_creation(certificate_signing_request=csr)
-
-    def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update({"certificate": event.certificate})
-        replicas_relation.data[self.app].update({"ca": event.ca})
-        replicas_relation.data[self.app].update({"chain": event.chain})
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-
-    def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-        replicas_relation.data[self.app].pop("certificate")
-        replicas_relation.data[self.app].pop("ca")
-        replicas_relation.data[self.app].pop("chain")
-        self.unit.status = WaitingStatus("Waiting for new certificate")
-
-
-if __name__ == "__main__":
-    main(ExampleRequirerCharm)
-```
-"""  # noqa: D405, D410, D411, D214, D416
-
-import copy
-import json
-import logging
-import uuid
-from datetime import datetime, timedelta
-from ipaddress import IPv4Address
-from typing import Dict, List, Optional
-
-from cryptography import x509
-from cryptography.hazmat._oid import ExtensionOID
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.x509.extensions import Extension, ExtensionNotFound
-from jsonschema import exceptions, validate  # type: ignore[import]
-from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent
-from ops.framework import EventBase, EventSource, Handle, Object
-
-# The unique Charmhub library identifier, never change it
-LIBID = "afd8c2bccf834997afce12c2706d2ede"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 12
-
-
-REQUIRER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` requirer root schema",
-    "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificate_signing_requests": [
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-            ]
-        }
-    ],
-    "properties": {
-        "certificate_signing_requests": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {"certificate_signing_request": {"type": "string"}},
-                "required": ["certificate_signing_request"],
-            },
-        }
-    },
-    "required": ["certificate_signing_requests"],
-    "additionalProperties": True,
-}
-
-PROVIDER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` provider root schema",
-    "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                }
-            ]
-        },
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                    "revoked": True,
-                }
-            ]
-        },
-    ],
-    "properties": {
-        "certificates": {
-            "$id": "#/properties/certificates",
-            "type": "array",
-            "items": {
-                "$id": "#/properties/certificates/items",
-                "type": "object",
-                "required": ["certificate_signing_request", "certificate", "ca", "chain"],
-                "properties": {
-                    "certificate_signing_request": {
-                        "$id": "#/properties/certificates/items/certificate_signing_request",
-                        "type": "string",
-                    },
-                    "certificate": {
-                        "$id": "#/properties/certificates/items/certificate",
-                        "type": "string",
-                    },
-                    "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
-                    "chain": {
-                        "$id": "#/properties/certificates/items/chain",
-                        "type": "array",
-                        "items": {
-                            "type": "string",
-                            "$id": "#/properties/certificates/items/chain/items",
-                        },
-                    },
-                    "revoked": {
-                        "$id": "#/properties/certificates/items/revoked",
-                        "type": "boolean",
-                    },
-                },
-                "additionalProperties": True,
-            },
-        }
-    },
-    "required": ["certificates"],
-    "additionalProperties": True,
-}
-
-
-logger = logging.getLogger(__name__)
-
-
-class CertificateAvailableEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is available."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-class CertificateExpiringEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is almost expired."""
-
-    def __init__(self, handle, certificate: str, expiry: str):
-        """CertificateExpiringEvent.
-
-        Args:
-            handle (Handle): Juju framework handle
-            certificate (str): TLS Certificate
-            expiry (str): Datetime string reprensenting the time at which the certificate
-                won't be valid anymore.
-        """
-        super().__init__(handle)
-        self.certificate = certificate
-        self.expiry = expiry
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate, "expiry": self.expiry}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.expiry = snapshot["expiry"]
-
-
-class CertificateExpiredEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is expired."""
-
-    def __init__(self, handle: Handle, certificate: str):
-        super().__init__(handle)
-        self.certificate = certificate
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-
-
-class CertificateRevokedEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        revoked: bool,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-        self.revoked = revoked
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-            "revoked": self.revoked,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-        self.revoked = snapshot["revoked"]
-
-
-class CertificateCreationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is required."""
-
-    def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int):
-        super().__init__(handle)
-        self.certificate_signing_request = certificate_signing_request
-        self.relation_id = relation_id
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate_signing_request": self.certificate_signing_request,
-            "relation_id": self.relation_id,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.relation_id = snapshot["relation_id"]
-
-
-class CertificateRevocationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate needs to be revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: str,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-def _load_relation_data(raw_relation_data: dict) -> dict:
-    """Loads relation data from the relation data bag.
-
-    Json loads all data.
-
-    Args:
-        raw_relation_data: Relation data from the databag
-
-    Returns:
-        dict: Relation data in dict format.
-    """
-    certificate_data = dict()
-    for key in raw_relation_data:
-        try:
-            certificate_data[key] = json.loads(raw_relation_data[key])
-        except (json.decoder.JSONDecodeError, TypeError):
-            certificate_data[key] = raw_relation_data[key]
-    return certificate_data
-
-
-def generate_ca(
-    private_key: bytes,
-    subject: str,
-    private_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    country: str = "US",
-) -> bytes:
-    """Generates a CA Certificate.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): Certificate subject
-        private_key_password (bytes): Private key password
-        validity (int): Certificate validity time (in days)
-        country (str): Certificate Issuing country
-
-    Returns:
-        bytes: CA Certificate.
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    subject = issuer = x509.Name(
-        [
-            x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
-            x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
-        ]
-    )
-    subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
-        private_key_object.public_key()  # type: ignore[arg-type]
-    )
-    subject_identifier = key_identifier = subject_identifier_object.public_bytes()
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(private_key_object.public_key())  # type: ignore[arg-type]
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-        .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
-        .add_extension(
-            x509.AuthorityKeyIdentifier(
-                key_identifier=key_identifier,
-                authority_cert_issuer=None,
-                authority_cert_serial_number=None,
-            ),
-            critical=False,
-        )
-        .add_extension(
-            x509.BasicConstraints(ca=True, path_length=None),
-            critical=True,
-        )
-        .sign(private_key_object, hashes.SHA256())  # type: ignore[arg-type]
-    )
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_certificate(
-    csr: bytes,
-    ca: bytes,
-    ca_key: bytes,
-    ca_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    alt_names: Optional[List[str]] = None,
-) -> bytes:
-    """Generates a TLS certificate based on a CSR.
-
-    Args:
-        csr (bytes): CSR
-        ca (bytes): CA Certificate
-        ca_key (bytes): CA private key
-        ca_key_password: CA private key password
-        validity (int): Certificate validity (in days)
-        alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
-
-    Returns:
-        bytes: Certificate
-    """
-    csr_object = x509.load_pem_x509_csr(csr)
-    subject = csr_object.subject
-    issuer = x509.load_pem_x509_certificate(ca).issuer
-    private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
-
-    certificate_builder = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(csr_object.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-    )
-
-    extensions_list = csr_object.extensions
-    san_ext: Optional[x509.Extension] = None
-    if alt_names:
-        full_sans_dns = alt_names.copy()
-        try:
-            loaded_san_ext = csr_object.extensions.get_extension_for_class(
-                x509.SubjectAlternativeName
-            )
-            full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName))
-        except ExtensionNotFound:
-            pass
-        finally:
-            san_ext = Extension(
-                ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
-                False,
-                x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]),
-            )
-            if not extensions_list:
-                extensions_list = x509.Extensions([san_ext])
-
-    for extension in extensions_list:
-        if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext:
-            extension = san_ext
-
-        certificate_builder = certificate_builder.add_extension(
-            extension.value,
-            critical=extension.critical,
-        )
-    certificate_builder._version = x509.Version.v3
-    cert = certificate_builder.sign(private_key, hashes.SHA256())  # type: ignore[arg-type]
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_pfx_package(
-    certificate: bytes,
-    private_key: bytes,
-    package_password: str,
-    private_key_password: Optional[bytes] = None,
-) -> bytes:
-    """Generates a PFX package to contain the TLS certificate and private key.
-
-    Args:
-        certificate (bytes): TLS certificate
-        private_key (bytes): Private key
-        package_password (str): Password to open the PFX package
-        private_key_password (bytes): Private key password
-
-    Returns:
-        bytes:
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    certificate_object = x509.load_pem_x509_certificate(certificate)
-    name = certificate_object.subject.rfc4514_string()
-    pfx_bytes = pkcs12.serialize_key_and_certificates(
-        name=name.encode(),
-        cert=certificate_object,
-        key=private_key_object,  # type: ignore[arg-type]
-        cas=None,
-        encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()),
-    )
-    return pfx_bytes
-
-
-def generate_private_key(
-    password: Optional[bytes] = None,
-    key_size: int = 2048,
-    public_exponent: int = 65537,
-) -> bytes:
-    """Generates a private key.
-
-    Args:
-        password (bytes): Password for decrypting the private key
-        key_size (int): Key size in bytes
-        public_exponent: Public exponent.
-
-    Returns:
-        bytes: Private Key
-    """
-    private_key = rsa.generate_private_key(
-        public_exponent=public_exponent,
-        key_size=key_size,
-    )
-    key_bytes = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.TraditionalOpenSSL,
-        encryption_algorithm=serialization.BestAvailableEncryption(password)
-        if password
-        else serialization.NoEncryption(),
-    )
-    return key_bytes
-
-
-def generate_csr(
-    private_key: bytes,
-    subject: str,
-    add_unique_id_to_subject_name: bool = True,
-    organization: Optional[str] = None,
-    email_address: Optional[str] = None,
-    country_name: Optional[str] = None,
-    private_key_password: Optional[bytes] = None,
-    sans: Optional[List[str]] = None,
-    sans_oid: Optional[List[str]] = None,
-    sans_ip: Optional[List[str]] = None,
-    sans_dns: Optional[List[str]] = None,
-    additional_critical_extensions: Optional[List] = None,
-) -> bytes:
-    """Generates a CSR using private key and subject.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): CSR Subject.
-        add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
-            subject name. Always leave to "True" when the CSR is used to request certificates
-            using the tls-certificates relation.
-        organization (str): Name of organization.
-        email_address (str): Email address.
-        country_name (str): Country Name.
-        private_key_password (bytes): Private key password
-        sans (list): Use sans_dns - this will be deprecated in a future release
-            List of DNS subject alternative names (keeping it for now for backward compatibility)
-        sans_oid (list): List of registered ID SANs
-        sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
-        sans_ip (list): List of IP subject alternative names
-        additional_critical_extensions (list): List if critical additional extension objects.
-            Object must be a x509 ExtensionType.
-
-    Returns:
-        bytes: CSR
-    """
-    signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
-    subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
-    if add_unique_id_to_subject_name:
-        unique_identifier = uuid.uuid4()
-        subject_name.append(
-            x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
-        )
-    if organization:
-        subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
-    if email_address:
-        subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
-    if country_name:
-        subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
-    csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
-
-    _sans: List[x509.GeneralName] = []
-    if sans_oid:
-        _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
-    if sans_ip:
-        _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
-    if sans:
-        _sans.extend([x509.DNSName(san) for san in sans])
-    if sans_dns:
-        _sans.extend([x509.DNSName(san) for san in sans_dns])
-    if _sans:
-        csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
-
-    if additional_critical_extensions:
-        for extension in additional_critical_extensions:
-            csr = csr.add_extension(extension, critical=True)
-
-    signed_certificate = csr.sign(signing_key, hashes.SHA256())  # type: ignore[arg-type]
-    return signed_certificate.public_bytes(serialization.Encoding.PEM)
-
-
-class CertificatesProviderCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates provider charm can leverage."""
-
-    certificate_creation_request = EventSource(CertificateCreationRequestEvent)
-    certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
-
-
-class CertificatesRequirerCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates requirer charm can leverage."""
-
-    certificate_available = EventSource(CertificateAvailableEvent)
-    certificate_expiring = EventSource(CertificateExpiringEvent)
-    certificate_expired = EventSource(CertificateExpiredEvent)
-    certificate_revoked = EventSource(CertificateRevokedEvent)
-
-
-class TLSCertificatesProvidesV1(Object):
-    """TLS certificates provider class to be instantiated by TLS certificates providers."""
-
-    on = CertificatesProviderCharmEvents()
-
-    def __init__(self, charm: CharmBase, relationship_name: str):
-        super().__init__(charm, relationship_name)
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.charm = charm
-        self.relationship_name = relationship_name
-
-    def _add_certificate(
-        self,
-        relation_id: int,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ) -> None:
-        """Adds certificate to relation data.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate Signing Request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_certificate = {
-            "certificate": certificate,
-            "certificate_signing_request": certificate_signing_request,
-            "ca": ca,
-            "chain": chain,
-        }
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        if new_certificate in certificates:
-            logger.info("Certificate already in relation data - Doing nothing")
-            return
-        certificates.append(new_certificate)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    def _remove_certificate(
-        self,
-        relation_id: int,
-        certificate: Optional[str] = None,
-        certificate_signing_request: Optional[str] = None,
-    ) -> None:
-        """Removes certificate from a given relation based on user provided certificate or csr.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate (optional)
-            certificate_signing_request: Certificate signing request (optional)
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name,
-            relation_id=relation_id,
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
-            )
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        for certificate_dict in certificates:
-            if certificate and certificate_dict["certificate"] == certificate:
-                certificates.remove(certificate_dict)
-            if (
-                certificate_signing_request
-                and certificate_dict["certificate_signing_request"] == certificate_signing_request
-            ):
-                certificates.remove(certificate_dict)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Uses JSON schema validator to validate relation data content.
-
-        Args:
-            certificates_data (dict): Certificate data dictionary as retrieved from relation data.
-
-        Returns:
-            bool: True/False depending on whether the relation data follows the json schema.
-        """
-        try:
-            validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def revoke_all_certificates(self) -> None:
-        """Revokes all certificates of this provider.
-
-        This method is meant to be used when the Root CA has changed.
-        """
-        for relation in self.model.relations[self.relationship_name]:
-            provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-            provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", []))
-            for certificate in provider_certificates:
-                certificate["revoked"] = True
-            relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates)
-
-    def set_relation_certificate(
-        self,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        relation_id: int,
-    ) -> None:
-        """Adds certificates to relation data.
-
-        Args:
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate signing request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-            relation_id (int): Juju relation ID
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        self._remove_certificate(
-            certificate_signing_request=certificate_signing_request.strip(),
-            relation_id=relation_id,
-        )
-        self._add_certificate(
-            relation_id=relation_id,
-            certificate=certificate.strip(),
-            certificate_signing_request=certificate_signing_request.strip(),
-            ca=ca.strip(),
-            chain=[cert.strip() for cert in chain],
-        )
-
-    def remove_certificate(self, certificate: str) -> None:
-        """Removes a given certificate from relation data.
-
-        Args:
-            certificate (str): TLS Certificate
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.relations[self.relationship_name]
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        for certificate_relation in certificates_relation:
-            self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed event.
-
-        Looks at the relation data and either emits:
-        - certificate request event: If the unit relation data contains a CSR for which
-            a certificate does not exist in the provider relation data.
-        - certificate revocation event: If the provider relation data contains a CSR for which
-            a csr does not exist in the requirer relation data.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        assert event.unit is not None
-        requirer_relation_data = _load_relation_data(event.relation.data[event.unit])
-        provider_relation_data = _load_relation_data(event.relation.data[self.charm.app])
-        if not self._relation_data_is_valid(requirer_relation_data):
-            logger.warning(
-                f"Relation data did not pass JSON Schema validation: {requirer_relation_data}"
-            )
-            return
-        provider_certificates = provider_relation_data.get("certificates", [])
-        requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-        provider_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in provider_certificates
-        ]
-        requirer_unit_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in requirer_csrs
-        ]
-        for certificate_signing_request in requirer_unit_csrs:
-            if certificate_signing_request not in provider_csrs:
-                self.on.certificate_creation_request.emit(
-                    certificate_signing_request=certificate_signing_request,
-                    relation_id=event.relation.id,
-                )
-        self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
-
-    def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
-        """Revokes certificates for which no unit has a CSR.
-
-        Goes through all generated certificates and compare agains the list of CSRS for all units
-        of a given relationship.
-
-        Args:
-            relation_id (int): Relation id
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app])
-        list_of_csrs: List[str] = []
-        for unit in certificates_relation.units:
-            requirer_relation_data = _load_relation_data(certificates_relation.data[unit])
-            requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-            list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs)
-        provider_certificates = provider_relation_data.get("certificates", [])
-        for certificate in provider_certificates:
-            if certificate["certificate_signing_request"] not in list_of_csrs:
-                self.on.certificate_revocation_request.emit(
-                    certificate=certificate["certificate"],
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-                self.remove_certificate(certificate=certificate["certificate"])
-
-
-class TLSCertificatesRequiresV1(Object):
-    """TLS certificates requirer class to be instantiated by TLS certificates requirers."""
-
-    on = CertificatesRequirerCharmEvents()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relationship_name: str,
-        expiry_notification_time: int = 168,
-    ):
-        """Generates/use private key and observes relation changed event.
-
-        Args:
-            charm: Charm object
-            relationship_name: Juju relation name
-            expiry_notification_time (int): Time difference between now and expiry (in hours).
-                Used to trigger the CertificateExpiring event. Default: 7 days.
-        """
-        super().__init__(charm, relationship_name)
-        self.relationship_name = relationship_name
-        self.charm = charm
-        self.expiry_notification_time = expiry_notification_time
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(charm.on.update_status, self._on_update_status)
-
-    @property
-    def _requirer_csrs(self) -> List[Dict[str, str]]:
-        """Returns list of requirer CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
-        return requirer_relation_data.get("certificate_signing_requests", [])
-
-    @property
-    def _provider_certificates(self) -> List[Dict[str, str]]:
-        """Returns list of provider CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        if not relation.app:
-            raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        return provider_relation_data.get("certificates", [])
-
-    def _add_requirer_csr(self, csr: str) -> None:
-        """Adds CSR to relation data.
-
-        Args:
-            csr (str): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_csr_dict = {"certificate_signing_request": csr}
-        if new_csr_dict in self._requirer_csrs:
-            logger.info("CSR already in relation data - Doing nothing")
-            return
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        requirer_csrs.append(new_csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def _remove_requirer_csr(self, csr: str) -> None:
-        """Removes CSR from relation data.
-
-        Args:
-            csr (str): Certificate signing request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        csr_dict = {"certificate_signing_request": csr}
-        if csr_dict not in requirer_csrs:
-            logger.info("CSR not in relation data - Doing nothing")
-            return
-        requirer_csrs.remove(csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def request_certificate_creation(self, certificate_signing_request: bytes) -> None:
-        """Request TLS certificate to provider charm.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            message = (
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-            logger.error(message)
-            raise RuntimeError(message)
-        self._add_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate request sent to provider")
-
-    def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
-        """Removes CSR from relation data.
-
-        The provider of this relation is then expected to remove certificates associated to this
-        CSR from the relation data as well and emit a request_certificate_revocation event for the
-        provider charm to interpret.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        self._remove_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate revocation sent to provider")
-
-    def request_certificate_renewal(
-        self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
-    ) -> None:
-        """Renews certificate.
-
-        Removes old CSR from relation data and adds new one.
-
-        Args:
-            old_certificate_signing_request: Old CSR
-            new_certificate_signing_request: New CSR
-
-        Returns:
-            None
-        """
-        try:
-            self.request_certificate_revocation(
-                certificate_signing_request=old_certificate_signing_request
-            )
-        except RuntimeError:
-            logger.warning("Certificate revocation failed.")
-        self.request_certificate_creation(
-            certificate_signing_request=new_certificate_signing_request
-        )
-        logger.info("Certificate renewal request completed.")
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Checks whether relation data is valid based on json schema.
-
-        Args:
-            certificates_data: Certificate data in dict format.
-
-        Returns:
-            bool: Whether relation data is valid.
-        """
-        try:
-            validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggered on relation changed events.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{event.relation.data[relation.app]}"
-            )
-            return
-        requirer_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in self._requirer_csrs
-        ]
-        for certificate in self._provider_certificates:
-            if certificate["certificate_signing_request"] in requirer_csrs:
-                if certificate.get("revoked", False):
-                    self.on.certificate_revoked.emit(
-                        certificate_signing_request=certificate["certificate_signing_request"],
-                        certificate=certificate["certificate"],
-                        ca=certificate["ca"],
-                        chain=certificate["chain"],
-                        revoked=True,
-                    )
-                else:
-                    self.on.certificate_available.emit(
-                        certificate_signing_request=certificate["certificate_signing_request"],
-                        certificate=certificate["certificate"],
-                        ca=certificate["ca"],
-                        chain=certificate["chain"],
-                    )
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Triggered on update status event.
-
-        Goes through each certificate in the "certificates" relation and checks their expiry date.
-        If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if
-        they are expired, emits a CertificateExpiredEvent.
-
-        Args:
-            event (UpdateStatusEvent): Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{relation.data[relation.app]}"
-            )
-            return
-        for certificate_dict in self._provider_certificates:
-            certificate = certificate_dict["certificate"]
-            try:
-                certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
-            except ValueError:
-                logger.warning("Could not load certificate.")
-                continue
-            time_difference = certificate_object.not_valid_after - datetime.utcnow()
-            if time_difference.total_seconds() < 0:
-                logger.warning("Certificate is expired")
-                self.on.certificate_expired.emit(certificate=certificate)
-                self.request_certificate_revocation(certificate.encode())
-                continue
-            if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60):
-                logger.warning("Certificate almost expired")
-                self.on.certificate_expiring.emit(
-                    certificate=certificate, expiry=certificate_object.not_valid_after.isoformat()
-                )
diff --git a/charms/openstack-exporter-k8s/osci.yaml b/charms/openstack-exporter-k8s/osci.yaml
deleted file mode 100644
index b56ba0eb..00000000
--- a/charms/openstack-exporter-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: openstack-exporter-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/openstack-exporter-k8s/pyproject.toml b/charms/openstack-exporter-k8s/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/openstack-exporter-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/openstack-exporter-k8s/rename.sh b/charms/openstack-exporter-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/openstack-exporter-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/openstack-exporter-k8s/requirements.txt b/charms/openstack-exporter-k8s/requirements.txt
index 3b37feec..48fb8600 100644
--- a/charms/openstack-exporter-k8s/requirements.txt
+++ b/charms/openstack-exporter-k8s/requirements.txt
@@ -1,5 +1,9 @@
 ops
 jinja2
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+lightkube
+lightkube-models
 # COS requirement
 cosl
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/openstack-exporter-k8s/test-requirements.txt b/charms/openstack-exporter-k8s/test-requirements.txt
deleted file mode 100644
index d1a61d34..00000000
--- a/charms/openstack-exporter-k8s/test-requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# This file is managed centrally.  If you find the need to modify this as a
-# one-off, please don't.  Intead, consult #openstack-charms and ask about
-# requirements management in charms via bot-control.  Thank you.
-
-coverage
-mock
-flake8
-stestr
-ops
diff --git a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py
index ecc89af4..88c4f24e 100644
--- a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py
+++ b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py
@@ -18,6 +18,7 @@
 
 import json
 
+import charm
 import ops_sunbeam.test_utils as test_utils
 from mock import (
     Mock,
@@ -26,8 +27,6 @@ from ops.testing import (
     Harness,
 )
 
-import charm
-
 
 class _OSExporterTestOperatorCharm(charm.OSExporterOperatorCharm):
     """Test Operator Charm for Openstack Exporter Operator."""
diff --git a/charms/openstack-exporter-k8s/tox.ini b/charms/openstack-exporter-k8s/tox.ini
deleted file mode 100644
index 0bc536c1..00000000
--- a/charms/openstack-exporter-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Operator charm (with zaza): tox.ini
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  HOME
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path}
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/openstack-hypervisor/.gitignore b/charms/openstack-hypervisor/.gitignore
deleted file mode 100644
index 33d25ac9..00000000
--- a/charms/openstack-hypervisor/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-venv/
-build/
-*.charm
-.tox/
-.coverage
-__pycache__/
-*.py[cod]
-.idea
-.vscode/
-.stestr/
diff --git a/charms/openstack-hypervisor/.gitreview b/charms/openstack-hypervisor/.gitreview
deleted file mode 100644
index afdea8f1..00000000
--- a/charms/openstack-hypervisor/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-openstack-hypervisor.git
-defaultbranch=main
diff --git a/charms/openstack-hypervisor/.stestr.conf b/charms/openstack-hypervisor/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/openstack-hypervisor/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/openstack-hypervisor/.zuul.yaml b/charms/openstack-hypervisor/.zuul.yaml
deleted file mode 100644
index fd8a2c1d..00000000
--- a/charms/openstack-hypervisor/.zuul.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - prometheus-alert-rules-test
-    vars:
-      charm_build_name: openstack-hypervisor
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
-      prometheus_alerts_test_rules_dir: tests/unit/test_alert_rules
diff --git a/charms/openstack-hypervisor/charmcraft.yaml b/charms/openstack-hypervisor/charmcraft.yaml
index c82f3030..9a8d8b29 100644
--- a/charms/openstack-hypervisor/charmcraft.yaml
+++ b/charms/openstack-hypervisor/charmcraft.yaml
@@ -23,4 +23,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py
deleted file mode 100644
index 016e1ba2..00000000
--- a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py
+++ /dev/null
@@ -1,224 +0,0 @@
-"""CeilometerServiceProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the ceilometer_service interface.
-
-Import `CeilometerServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "ceilometer_service"
-
-Two events are also available to respond to:
-    - config_changed
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.ceilometer_k8s.v0.ceilometer_service import (
-    CeilometerServiceRequires
-)
-
-class CeilometerServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # CeilometerService Requires
-        self.ceilometer_service = CeilometerServiceRequires(
-            self, "ceilometer_service",
-        )
-        self.framework.observe(
-            self.ceilometer_service.on.config_changed,
-            self._on_ceilometer_service_config_changed
-        )
-        self.framework.observe(
-            self.ceilometer_service.on.goneaway,
-            self._on_ceiometer_service_goneaway
-        )
-
-    def _on_ceilometer_service_config_changed(self, event):
-        '''React to the Ceilometer service config changed event.
-
-        This event happens when CeilometerService relation is added to the
-        model and relation data is changed.
-        '''
-        # Do something with the configuration provided by relation.
-        pass
-
-    def _on_ceilometer_service_goneaway(self, event):
-        '''React to the CeilometerService goneaway event.
-
-        This event happens when CeilometerService relation is removed.
-        '''
-        # CeilometerService Relation has goneaway.
-        pass
-```
-"""
-
-import logging
-from typing import (
-    Optional,
-)
-
-from ops.charm import (
-    CharmBase,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationEvent,
-)
-from ops.framework import (
-    EventSource,
-    Object,
-    ObjectEvents,
-)
-from ops.model import (
-    Relation,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "fcbb94e7a18740729eaf9e2c3b90017f"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-class CeilometerConfigRequestEvent(RelationEvent):
-    """CeilometerConfigRequest Event."""
-
-    pass
-
-
-class CeilometerServiceProviderEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    config_request = EventSource(CeilometerConfigRequestEvent)
-
-
-class CeilometerServiceProvides(Object):
-    """CeilometerServiceProvides class."""
-
-    on = CeilometerServiceProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ceilometer_service_relation_changed,
-        )
-
-    def _on_ceilometer_service_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle CeilometerService relation changed."""
-        logging.debug("CeilometerService relation changed")
-        self.on.config_request.emit(event.relation)
-
-    def set_config(
-        self, relation: Optional[Relation], telemetry_secret: str
-    ) -> None:
-        """Set ceilometer configuration on the relation."""
-        if not self.charm.unit.is_leader():
-            logging.debug("Not a leader unit, skipping set config")
-            return
-
-        # If relation is not provided send config to all the related
-        # applications. This happens usually when config data is
-        # updated by provider and wants to send the data to all
-        # related applications
-        if relation is None:
-            logging.debug(
-                "Sending config to all related applications of relation"
-                f"{self.relation_name}"
-            )
-            for relation in self.framework.model.relations[self.relation_name]:
-                relation.data[self.charm.app][
-                    "telemetry-secret"
-                ] = telemetry_secret
-        else:
-            logging.debug(
-                f"Sending config on relation {relation.app.name} "
-                f"{relation.name}/{relation.id}"
-            )
-            relation.data[self.charm.app][
-                "telemetry-secret"
-            ] = telemetry_secret
-
-
-class CeilometerConfigChangedEvent(RelationEvent):
-    """CeilometerConfigChanged Event."""
-
-    pass
-
-
-class CeilometerServiceGoneAwayEvent(RelationEvent):
-    """CeilometerServiceGoneAway Event."""
-
-    pass
-
-
-class CeilometerServiceRequirerEvents(ObjectEvents):
-    """Events class for `on`."""
-
-    config_changed = EventSource(CeilometerConfigChangedEvent)
-    goneaway = EventSource(CeilometerServiceGoneAwayEvent)
-
-
-class CeilometerServiceRequires(Object):
-    """CeilometerServiceRequires class."""
-
-    on = CeilometerServiceRequirerEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ceilometer_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ceilometer_service_relation_broken,
-        )
-
-    def _on_ceilometer_service_relation_changed(
-        self, event: RelationChangedEvent
-    ):
-        """Handle CeilometerService relation changed."""
-        logging.debug("CeilometerService config data changed")
-        self.on.config_changed.emit(event.relation)
-
-    def _on_ceilometer_service_relation_broken(
-        self, event: RelationBrokenEvent
-    ):
-        """Handle CeilometerService relation changed."""
-        logging.debug("CeilometerService on_broken")
-        self.on.goneaway.emit(event.relation)
-
-    @property
-    def _ceilometer_service_rel(self) -> Optional[Relation]:
-        """The ceilometer service relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> Optional[str]:
-        """Return the value for the given key from remote app data."""
-        if self._ceilometer_service_rel:
-            data = self._ceilometer_service_rel.data[
-                self._ceilometer_service_rel.app
-            ]
-            return data.get(key)
-
-        return None
-
-    @property
-    def telemetry_secret(self) -> Optional[str]:
-        """Return the telemetry_secret."""
-        return self.get_remote_app_data("telemetry-secret")
diff --git a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py
deleted file mode 100644
index 94c5fe6d..00000000
--- a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py
+++ /dev/null
@@ -1,266 +0,0 @@
-"""CephAccess Provides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the ceph-access interface.
-
-Import `CephAccessRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "ceph_access"
-
-Three events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.cinder_ceph_k8s.v0.ceph_access import CephAccessRequires
-
-class CephAccessClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # CephAccess Requires
-        self.ceph_access = CephAccessRequires(
-            self,
-            relation_name="ceph_access",
-        )
-        self.framework.observe(
-            self.ceph_access.on.connected, self._on_ceph_access_connected)
-        self.framework.observe(
-            self.ceph_access.on.ready, self._on_ceph_access_ready)
-        self.framework.observe(
-            self.ceph_access.on.goneaway, self._on_ceph_access_goneaway)
-
-    def _on_ceph_access_connected(self, event):
-        '''React to the CephAccess connected event.
-
-        This event happens when n CephAccess relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_ceph_access_ready(self, event):
-        '''React to the CephAccess ready event.
-
-        This event happens when an CephAccess relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-
-```
-
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "7fa8d4f8407c4f31ab1deb51c0c046f1"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-from ops.framework import (
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-logger = logging.getLogger(__name__)
-
-class CephAccessConnectedEvent(EventBase):
-    """CephAccess connected Event."""
-
-    pass
-
-
-class CephAccessReadyEvent(EventBase):
-    """CephAccess ready for use Event."""
-
-    pass
-
-
-class CephAccessGoneAwayEvent(EventBase):
-    """CephAccess relation has gone-away Event"""
-
-    pass
-
-
-class CephAccessServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(CephAccessConnectedEvent)
-    ready = EventSource(CephAccessReadyEvent)
-    goneaway = EventSource(CephAccessGoneAwayEvent)
-
-
-class CephAccessRequires(Object):
-    """
-    CephAccessRequires class
-    """
-
-
-    on = CephAccessServerEvents()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ceph_access_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ceph_access_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_ceph_access_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ceph_access_relation_broken,
-        )
-
-    @property
-    def _ceph_access_rel(self) -> Relation:
-        """The CephAccess relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._ceph_access_rel.data[self._ceph_access_rel.app]
-        return data.get(key)
-
-    def _on_ceph_access_relation_joined(self, event):
-        """CephAccess relation joined."""
-        logging.debug("CephAccess on_joined")
-        self.on.connected.emit()
-
-    def _on_ceph_access_relation_changed(self, event):
-        """CephAccess relation changed."""
-        logging.debug("CephAccess on_changed")
-        try:
-            if self.ready:
-                self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_ceph_access_relation_broken(self, event):
-        """CephAccess relation broken."""
-        logging.debug("CephAccess on_broken")
-        self.on.goneaway.emit()
-
-    def _retrieve_secret(self):
-        try:
-            credentials_id = self.get_remote_app_data('access-credentials')
-            if not credentials_id:
-                return None
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def ceph_access_data(self):
-        """Return the service_password."""
-        secret = self._retrieve_secret()
-        if not secret:
-            return {}
-        return secret.get_content()
-
-    @property
-    def ready(self) -> str:
-        """Return the service_password."""
-        return all(k in self.ceph_access_data for k in ["uuid", "key"])
-
-class HasCephAccessClientsEvent(EventBase):
-    """Has CephAccessClients Event."""
-
-    pass
-
-class ReadyCephAccessClientsEvent(EventBase):
-    """Has CephAccessClients Event."""
-
-    def __init__(self, handle, relation_id):
-        super().__init__(handle)
-        self.relation_id = relation_id
-
-    def snapshot(self):
-        return {"relation_id": self.relation_id}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-
-class CephAccessClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_ceph_access_clients = EventSource(HasCephAccessClientsEvent)
-    ready_ceph_access_clients = EventSource(ReadyCephAccessClientsEvent)
-
-
-class CephAccessProvides(Object):
-    """
-    CephAccessProvides class
-    """
-
-    on = CephAccessClientEvents()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ceph_access_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ceph_access_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ceph_access_relation_broken,
-        )
-
-    def _on_ceph_access_relation_joined(self, event):
-        """Handle CephAccess joined."""
-        logging.debug("CephAccess on_joined")
-        self.on.has_ceph_access_clients.emit()
-
-    def _on_ceph_access_relation_changed(self, event):
-        """Handle CephAccess joined."""
-        logging.debug("CephAccess on_changed")
-        self.on.ready_ceph_access_clients.emit(event.relation.id)
-
-    def _on_ceph_access_relation_broken(self, event):
-        """Handle CephAccess broken."""
-        logging.debug("CephAccessProvides on_broken")
-
-    def set_ceph_access_credentials(self, relation_name: int,
-                                    relation_id: str,
-                                    access_credentials: str):
-
-        logging.debug("Setting ceph_access connection information.")
-        _ceph_access_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _ceph_access_rel = relation
-        if not _ceph_access_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _ceph_access_rel.data[self.charm.app]
-        logging.debug(access_credentials)
-        app_data["access-credentials"] = access_credentials
diff --git a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py b/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py
deleted file mode 100644
index 6253f738..00000000
--- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py
+++ /dev/null
@@ -1,418 +0,0 @@
-"""CloudCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the cloud_credentials interface.
-
-Import `CloudCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "cloud_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires
-
-class CloudCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # CloudCredentials Requires
-        self.cloud_credentials = CloudCredentialsRequires(
-            self, "cloud_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.cloud_credentials.on.connected, self._on_cloud_credentials_connected)
-        self.framework.observe(
-            self.cloud_credentials.on.ready, self._on_cloud_credentials_ready)
-        self.framework.observe(
-            self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway)
-
-    def _on_cloud_credentials_connected(self, event):
-        '''React to the CloudCredentials connected event.
-
-        This event happens when n CloudCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_cloud_credentials_ready(self, event):
-        '''React to the CloudCredentials ready event.
-
-        The CloudCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # CloudCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_cloud_credentials_goneaway(self, event):
-        '''React to the CloudCredentials goneaway event.
-
-        This event happens when an CloudCredentials relation is removed.
-        '''
-        # CloudCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "a5d96cc2686c47eea554ce2210c2d24e"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 2
-
-logger = logging.getLogger(__name__)
-
-
-class CloudCredentialsConnectedEvent(EventBase):
-    """CloudCredentials connected Event."""
-
-    pass
-
-
-class CloudCredentialsReadyEvent(EventBase):
-    """CloudCredentials ready for use Event."""
-
-    pass
-
-
-class CloudCredentialsGoneAwayEvent(EventBase):
-    """CloudCredentials relation has gone-away Event"""
-
-    pass
-
-
-class CloudCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(CloudCredentialsConnectedEvent)
-    ready = EventSource(CloudCredentialsReadyEvent)
-    goneaway = EventSource(CloudCredentialsGoneAwayEvent)
-
-
-class CloudCredentialsRequires(Object):
-    """
-    CloudCredentialsRequires class
-    """
-
-    on = CloudCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """CloudCredentials relation joined."""
-        logging.debug("CloudCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """CloudCredentials relation changed."""
-        logging.debug("CloudCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """CloudCredentials relation broken."""
-        logging.debug("CloudCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _cloud_credentials_rel(self) -> Relation:
-        """The CloudCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def username(self) -> str:
-        """Return the username."""
-        return self.get_remote_app_data('username')
-
-    @property
-    def password(self) -> str:
-        """Return the password."""
-        return self.get_remote_app_data('password')
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the CloudCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._cloud_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasCloudCredentialsClientsEvent(EventBase):
-    """Has CloudCredentialsClients Event."""
-
-    pass
-
-
-class ReadyCloudCredentialsClientsEvent(EventBase):
-    """CloudCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class CloudCredentialsClientsGoneAwayEvent(EventBase):
-    """Has CloudCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class CloudCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_cloud_credentials_clients = EventSource(
-        HasCloudCredentialsClientsEvent
-    )
-    ready_cloud_credentials_clients = EventSource(
-        ReadyCloudCredentialsClientsEvent
-    )
-    cloud_credentials_clients_gone = EventSource(
-        CloudCredentialsClientsGoneAwayEvent
-    )
-
-
-class CloudCredentialsProvides(Object):
-    """
-    CloudCredentialsProvides class
-    """
-
-    on = CloudCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """Handle CloudCredentials joined."""
-        logging.debug("CloudCredentialsProvides on_joined")
-        self.on.has_cloud_credentials_clients.emit()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """Handle CloudCredentials changed."""
-        logging.debug("CloudCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_cloud_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """Handle CloudCredentials broken."""
-        logging.debug("CloudCredentialsProvides on_departed")
-        self.on.cloud_credentials_clients_gone.emit()
-
-    def set_cloud_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              username: str,
-                              password: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str):
-        logging.debug("Setting cloud_credentials connection information.")
-        _cloud_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _cloud_credentials_rel = relation
-        if not _cloud_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _cloud_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["username"] = username
-        app_data["password"] = password
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py
deleted file mode 100644
index e3f4565d..00000000
--- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py
+++ /dev/null
@@ -1,458 +0,0 @@
-"""IdentityCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_credentials interface.
-
-Import `IdentityCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires
-
-class IdentityCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityCredentials Requires
-        self.identity_credentials = IdentityCredentialsRequires(
-            self, "identity_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_credentials.on.connected, self._on_identity_credentials_connected)
-        self.framework.observe(
-            self.identity_credentials.on.ready, self._on_identity_credentials_ready)
-        self.framework.observe(
-            self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway)
-
-    def _on_identity_credentials_connected(self, event):
-        '''React to the IdentityCredentials connected event.
-
-        This event happens when IdentityCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_credentials_ready(self, event):
-        '''React to the IdentityCredentials ready event.
-
-        The IdentityCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_credentials_goneaway(self, event):
-        '''React to the IdentityCredentials goneaway event.
-
-        This event happens when an IdentityCredentials relation is removed.
-        '''
-        # IdentityCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityCredentialsConnectedEvent(EventBase):
-    """IdentityCredentials connected Event."""
-
-    pass
-
-
-class IdentityCredentialsReadyEvent(EventBase):
-    """IdentityCredentials ready for use Event."""
-
-    pass
-
-
-class IdentityCredentialsGoneAwayEvent(EventBase):
-    """IdentityCredentials relation has gone-away Event"""
-
-    pass
-
-
-class IdentityCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityCredentialsConnectedEvent)
-    ready = EventSource(IdentityCredentialsReadyEvent)
-    goneaway = EventSource(IdentityCredentialsGoneAwayEvent)
-
-
-class IdentityCredentialsRequires(Object):
-    """
-    IdentityCredentialsRequires class
-    """
-
-    on = IdentityCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """IdentityCredentials relation joined."""
-        logging.debug("IdentityCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """IdentityCredentials relation changed."""
-        logging.debug("IdentityCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """IdentityCredentials relation broken."""
-        logging.debug("IdentityCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_credentials_rel(self) -> Relation:
-        """The IdentityCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_credentials_rel.data[self._identity_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def credentials(self) -> str:
-        return self.get_remote_app_data('credentials')
-
-    @property
-    def username(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def password(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    @property
-    def internal_endpoint(self) -> str:
-        """Return the region for the internal auth url."""
-        return self.get_remote_app_data('internal-endpoint')
-
-    @property
-    def public_endpoint(self) -> str:
-        """Return the region for the public auth url."""
-        return self.get_remote_app_data('public-endpoint')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the IdentityCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._identity_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasIdentityCredentialsClientsEvent(EventBase):
-    """Has IdentityCredentialsClients Event."""
-
-    pass
-
-
-class ReadyIdentityCredentialsClientsEvent(EventBase):
-    """IdentityCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class IdentityCredentialsClientsGoneAwayEvent(EventBase):
-    """Has IdentityCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class IdentityCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_credentials_clients = EventSource(
-        HasIdentityCredentialsClientsEvent
-    )
-    ready_identity_credentials_clients = EventSource(
-        ReadyIdentityCredentialsClientsEvent
-    )
-    identity_credentials_clients_gone = EventSource(
-        IdentityCredentialsClientsGoneAwayEvent
-    )
-
-
-class IdentityCredentialsProvides(Object):
-    """
-    IdentityCredentialsProvides class
-    """
-
-    on = IdentityCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_credentials_relation_broken,
-        )
-
-    def _on_identity_credentials_relation_joined(self, event):
-        """Handle IdentityCredentials joined."""
-        logging.debug("IdentityCredentialsProvides on_joined")
-        self.on.has_identity_credentials_clients.emit()
-
-    def _on_identity_credentials_relation_changed(self, event):
-        """Handle IdentityCredentials changed."""
-        logging.debug("IdentityCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_identity_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_identity_credentials_relation_broken(self, event):
-        """Handle IdentityCredentials broken."""
-        logging.debug("IdentityCredentialsProvides on_departed")
-        self.on.identity_credentials_clients_gone.emit()
-
-    def set_identity_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              credentials: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str,
-                              admin_role: str):
-        logging.debug("Setting identity_credentials connection information.")
-        _identity_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_credentials_rel = relation
-        if not _identity_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _identity_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["credentials"] = credentials
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
-        app_data["internal-endpoint"] = self.charm.internal_endpoint
-        app_data["public-endpoint"] = self.charm.public_endpoint
-        app_data["admin-role"] = admin_role
diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py
deleted file mode 100644
index e8d2773e..00000000
--- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py
+++ /dev/null
@@ -1,493 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import Relation
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 2
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        return self.get_remote_app_data('service-password')
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        return self.get_remote_app_data('service-user-name')
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_password: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-name"] = service_user.name
-        app_data["service-user-id"] = service_user.id
-        app_data["service-password"] = service_password
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py
deleted file mode 100644
index 75ece456..00000000
--- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py
+++ /dev/null
@@ -1,451 +0,0 @@
-"""CloudCredentialsProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the cloud_credentials interface.
-
-Import `CloudCredentialsRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "cloud_credentials"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires
-
-class CloudCredentialsClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # CloudCredentials Requires
-        self.cloud_credentials = CloudCredentialsRequires(
-            self, "cloud_credentials",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.cloud_credentials.on.connected, self._on_cloud_credentials_connected)
-        self.framework.observe(
-            self.cloud_credentials.on.ready, self._on_cloud_credentials_ready)
-        self.framework.observe(
-            self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway)
-
-    def _on_cloud_credentials_connected(self, event):
-        '''React to the CloudCredentials connected event.
-
-        This event happens when n CloudCredentials relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_cloud_credentials_ready(self, event):
-        '''React to the CloudCredentials ready event.
-
-        The CloudCredentials interface will use the provided config for the
-        request to the identity server.
-        '''
-        # CloudCredentials Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_cloud_credentials_goneaway(self, event):
-        '''React to the CloudCredentials goneaway event.
-
-        This event happens when an CloudCredentials relation is removed.
-        '''
-        # CloudCredentials Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "a5d96cc2686c47eea554ce2210c2d24e"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-logger = logging.getLogger(__name__)
-
-
-class CloudCredentialsConnectedEvent(EventBase):
-    """CloudCredentials connected Event."""
-
-    pass
-
-
-class CloudCredentialsReadyEvent(EventBase):
-    """CloudCredentials ready for use Event."""
-
-    pass
-
-
-class CloudCredentialsGoneAwayEvent(EventBase):
-    """CloudCredentials relation has gone-away Event"""
-
-    pass
-
-
-class CloudCredentialsServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(CloudCredentialsConnectedEvent)
-    ready = EventSource(CloudCredentialsReadyEvent)
-    goneaway = EventSource(CloudCredentialsGoneAwayEvent)
-
-
-class CloudCredentialsRequires(Object):
-    """
-    CloudCredentialsRequires class
-    """
-
-    on = CloudCredentialsServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """CloudCredentials relation joined."""
-        logging.debug("CloudCredentials on_joined")
-        self.on.connected.emit()
-        self.request_credentials()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """CloudCredentials relation changed."""
-        logging.debug("CloudCredentials on_changed")
-        try:
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            logger.exception('Error when emitting event')
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """CloudCredentials relation broken."""
-        logging.debug("CloudCredentials on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _cloud_credentials_rel(self) -> Relation:
-        """The CloudCredentials relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def credentials(self) -> str:
-        return self.get_remote_app_data('credentials')
-
-    @property
-    def username(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def password(self) -> str:
-        credentials_id = self.get_remote_app_data('credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def project_name(self) -> str:
-        """Return the project name."""
-        return self.get_remote_app_data('project-name')
-
-    @property
-    def project_id(self) -> str:
-        """Return the project id."""
-        return self.get_remote_app_data('project-id')
-
-    @property
-    def user_domain_name(self) -> str:
-        """Return the name of the user domain."""
-        return self.get_remote_app_data('user-domain-name')
-
-    @property
-    def user_domain_id(self) -> str:
-        """Return the id of the user domain."""
-        return self.get_remote_app_data('user-domain-id')
-
-    @property
-    def project_domain_name(self) -> str:
-        """Return the name of the project domain."""
-        return self.get_remote_app_data('project-domain-name')
-
-    @property
-    def project_domain_id(self) -> str:
-        """Return the id of the project domain."""
-        return self.get_remote_app_data('project-domain-id')
-
-    @property
-    def region(self) -> str:
-        """Return the region for the auth urls."""
-        return self.get_remote_app_data('region')
-
-    @property
-    def internal_endpoint(self) -> str:
-        """Return the region for the internal auth url."""
-        return self.get_remote_app_data('internal-endpoint')
-
-    @property
-    def public_endpoint(self) -> str:
-        """Return the region for the public auth url."""
-        return self.get_remote_app_data('public-endpoint')
-
-    def request_credentials(self) -> None:
-        """Request credentials from the CloudCredentials server."""
-        if self.model.unit.is_leader():
-            logging.debug(f'Requesting credentials for {self.charm.app.name}')
-            app_data = self._cloud_credentials_rel.data[self.charm.app]
-            app_data['username'] = self.charm.app.name
-
-
-class HasCloudCredentialsClientsEvent(EventBase):
-    """Has CloudCredentialsClients Event."""
-
-    pass
-
-
-class ReadyCloudCredentialsClientsEvent(EventBase):
-    """CloudCredentialsClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, username):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.username = username
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "username": self.username,
-        }
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.username = snapshot["username"]
-
-
-class CloudCredentialsClientsGoneAwayEvent(EventBase):
-    """Has CloudCredentialsClientsGoneAwayEvent Event."""
-
-    pass
-
-
-class CloudCredentialsClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_cloud_credentials_clients = EventSource(
-        HasCloudCredentialsClientsEvent
-    )
-    ready_cloud_credentials_clients = EventSource(
-        ReadyCloudCredentialsClientsEvent
-    )
-    cloud_credentials_clients_gone = EventSource(
-        CloudCredentialsClientsGoneAwayEvent
-    )
-
-
-class CloudCredentialsProvides(Object):
-    """
-    CloudCredentialsProvides class
-    """
-
-    on = CloudCredentialsClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_cloud_credentials_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_cloud_credentials_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_cloud_credentials_relation_broken,
-        )
-
-    def _on_cloud_credentials_relation_joined(self, event):
-        """Handle CloudCredentials joined."""
-        logging.debug("CloudCredentialsProvides on_joined")
-        self.on.has_cloud_credentials_clients.emit()
-
-    def _on_cloud_credentials_relation_changed(self, event):
-        """Handle CloudCredentials changed."""
-        logging.debug("CloudCredentials on_changed")
-        REQUIRED_KEYS = ['username']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            username = event.relation.data[event.relation.app]['username']
-            self.on.ready_cloud_credentials_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                username,
-            )
-
-    def _on_cloud_credentials_relation_broken(self, event):
-        """Handle CloudCredentials broken."""
-        logging.debug("CloudCredentialsProvides on_departed")
-        self.on.cloud_credentials_clients_gone.emit()
-
-    def set_cloud_credentials(self, relation_name: int,
-                              relation_id: str,
-                              api_version: str,
-                              auth_host: str,
-                              auth_port: str,
-                              auth_protocol: str,
-                              internal_host: str,
-                              internal_port: str,
-                              internal_protocol: str,
-                              credentials: str,
-                              project_name: str,
-                              project_id: str,
-                              user_domain_name: str,
-                              user_domain_id: str,
-                              project_domain_name: str,
-                              project_domain_id: str,
-                              region: str):
-        logging.debug("Setting cloud_credentials connection information.")
-        _cloud_credentials_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _cloud_credentials_rel = relation
-        if not _cloud_credentials_rel:
-            # Relation has disappeared so don't send the data
-            return
-        app_data = _cloud_credentials_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["credentials"] = credentials
-        app_data["project-name"] = project_name
-        app_data["project-id"] = project_id
-        app_data["user-domain-name"] = user_domain_name
-        app_data["user-domain-id"] = user_domain_id
-        app_data["project-domain-name"] = project_domain_name
-        app_data["project-domain-id"] = project_domain_id
-        app_data["region"] = region
-        app_data["internal-endpoint"] = self.charm.internal_endpoint
-        app_data["public-endpoint"] = self.charm.public_endpoint
diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 35556622..00000000
--- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,518 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 0
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
diff --git a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py
deleted file mode 100644
index a3fb9109..00000000
--- a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py
+++ /dev/null
@@ -1,280 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will
-be overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of tuples that each define
-a port for the service, where each tuple contains:
-
-- a name for the port
-- port for the service to listen on
-- optionally: a targetPort for the service (the port in the container!)
-- optionally: a nodePort for the service (for NodePort or LoadBalancer services only!)
-- optionally: a name of the service (in case service name needs to be patched as well)
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For ClusterIP services:
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)])
-    # ...
-```
-
-For LoadBalancer/NodePort services:
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.service_patcher = KubernetesServicePatch(
-        self, [(f"{self.app.name}", 443, 443, 30666)], "LoadBalancer"
-    )
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import Literal, Sequence, Tuple, Union
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PortDefinition = Union[Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]]
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: Sequence[PortDefinition],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of tuples (name, port, targetPort, nodePort) for every service port.
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: Sequence[PortDefinition],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of tuples of the form (name, port) or (name, port, targetPort)
-                or (name, port, targetPort, nodePort) for every service port. If the 'targetPort'
-                is omitted, it is assumed to be equal to 'port', with the exception of NodePort
-                and LoadBalancer services, where all port numbers have to be specified.
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=[
-                    ServicePort(
-                        name=p[0],
-                        port=p[1],
-                        targetPort=p[2] if len(p) > 2 else p[1],  # type: ignore[misc]
-                        nodePort=p[3] if len(p) > 3 else None,  # type: ignore[arg-type, misc]
-                    )
-                    for p in ports
-                ],
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644
index 56cca01a..00000000
--- a/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py
+++ /dev/null
@@ -1,342 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
-cat << EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Bound with custom events by providing `refresh_event` argument:
-For example, you would like to have a configurable port in your charm and want to apply
-service patch every time charm config is changed.
-
-```python
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(
-        self,
-        [port],
-        refresh_event=self.on.config_changed
-    )
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal, Optional, Union
-
-from lightkube import ApiError, Client
-from lightkube.core import exceptions
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import BoundEvent, Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: Optional[str] = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: Optional[dict] = None,
-        additional_selectors: Optional[dict] = None,
-        additional_annotations: Optional[dict] = None,
-        *,
-        refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-            refresh_event: an optional bound event or list of bound events which
-                will be observed to re-apply the patch (e.g. on port change).
-                The `install` and `upgrade-charm` events would be observed regardless.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-        self.framework.observe(charm.on.update_status, self._patch)
-
-        # apply user defined events
-        if refresh_event:
-            if not isinstance(refresh_event, list):
-                refresh_event = [refresh_event]
-
-            for evt in refresh_event:
-                self.framework.observe(evt, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: Optional[str] = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: Optional[dict] = None,
-        additional_selectors: Optional[dict] = None,
-        additional_annotations: Optional[dict] = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        try:
-            client = Client()
-        except exceptions.ConfigError as e:
-            logger.warning("Error creating k8s client: %s", e)
-            return
-
-        try:
-            if self._is_patched(client):
-                return
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        return self._is_patched(client)
-
-    def _is_patched(self, client: Client) -> bool:
-        # Get the relevant service from the cluster
-        try:
-            service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        except ApiError as e:
-            if e.status.code == 404 and self.service_name != self._app:
-                return False
-            else:
-                logger.error("Kubernetes service get failed: %s", str(e))
-                raise
-
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [
-            (p.port, p.targetPort) for p in service.spec.ports  # type: ignore[attr-defined]
-        ]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py
deleted file mode 100644
index ef016e23..00000000
--- a/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py
+++ /dev/null
@@ -1,218 +0,0 @@
-"""TODO: Add a proper docstring here.
-
-This is a placeholder docstring for this charm library. Docstrings are
-presented on Charmhub and updated whenever you push a new version of the
-library.
-
-Complete documentation about creating and documenting libraries can be found
-in the SDK docs at https://juju.is/docs/sdk/libraries.
-
-See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
-share and consume charm libraries. They serve to enhance collaboration
-between charmers. Use a charmer's libraries for classes that handle
-integration with their charm.
-
-Bear in mind that new revisions of the different major API versions (v0, v1,
-v2 etc) are maintained independently.  You can continue to update v0 and v1
-after you have pushed v3.
-
-Markdown is supported, following the CommonMark specification.
-"""
-
-import logging
-import typing
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "114b7bb1970445daa61650e451f9da62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-# TODO: add your code here! Happy coding!
-class OVSDBCMSConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSConnectedEvent)
-    ready = EventSource(OVSDBCMSReadyEvent)
-    goneaway = EventSource(OVSDBCMSGoneAwayEvent)
-
-
-class OVSDBCMSRequires(Object):
-    """
-    OVSDBCMSRequires class
-    """
-
-    on = OVSDBCMSServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """OVSDBCMS relation joined."""
-        logging.debug("OVSDBCMSRequires on_joined")
-        self.on.connected.emit()
-
-    def bound_hostnames(self):
-        return self.get_all_unit_values("bound-hostname")
-
-    def bound_addresses(self):
-        return self.get_all_unit_values("bound-address")
-
-    def public_address(self):
-        relation = self.framework.model.get_relation(self.relation_name)
-        data = relation.data[relation.app]
-        return data.get('public-address')
-
-    def remote_ready(self):
-        return all(self.bound_hostnames()) or all(self.bound_addresses())
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """OVSDBCMS relation changed."""
-        logging.debug("OVSDBCMSRequires on_changed")
-        if self.remote_ready():
-            self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """OVSDBCMS relation broken."""
-        logging.debug("OVSDBCMSRequires on_broken")
-        self.on.goneaway.emit()
-
-    def get_all_unit_values(self, key: str) -> typing.List[str]:
-        """Retrieve value for key from all related units."""
-        values = []
-        relation = self.framework.model.get_relation(self.relation_name)
-        if relation:
-            for unit in relation.units:
-                values.append(relation.data[unit].get(key))
-        return values
-
-
-
-class OVSDBCMSClientConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSClientReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSClientGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSClientConnectedEvent)
-    ready = EventSource(OVSDBCMSClientReadyEvent)
-    goneaway = EventSource(OVSDBCMSClientGoneAwayEvent)
-
-
-class OVSDBCMSProvides(Object):
-    """
-    OVSDBCMSProvides class
-    """
-
-    on = OVSDBCMSClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """Handle ovsdb-cms joined."""
-        logging.debug("OVSDBCMSProvides on_joined")
-        self.on.connected.emit()
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """Handle ovsdb-cms changed."""
-        logging.debug("OVSDBCMSProvides on_changed")
-        self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """Handle ovsdb-cms broken."""
-        logging.debug("OVSDBCMSProvides on_departed")
-        self.on.goneaway.emit()
-
-    def set_unit_data(self, settings: typing.Dict[str, str]) -> None:
-        """Publish settings on the peer unit data bag."""
-        relations = self.framework.model.relations[self.relation_name]
-        for relation in relations:
-            for k, v in settings.items():
-                relation.data[self.model.unit][k] = v
-
-    def set_app_data(self, settings: typing.Dict[str, str]) -> None:
-        """Publish settings on the app data bag."""
-        relations = self.framework.model.relations[self.relation_name]
-        for relation in relations:
-            for k, v in settings.items():
-                relation.data[self.charm.app][k] = v
diff --git a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py
deleted file mode 100644
index 1eda19bf..00000000
--- a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py
+++ /dev/null
@@ -1,1261 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Library for the tls-certificates relation.
-
-This library contains the Requires and Provides classes for handling the tls-certificates
-interface.
-
-## Getting Started
-From a charm directory, fetch the library using `charmcraft`:
-
-```shell
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-```
-
-Add the following libraries to the charm's `requirements.txt` file:
-- jsonschema
-- cryptography
-
-Add the following section to the charm's `charmcraft.yaml` file:
-```yaml
-parts:
-  charm:
-    build-packages:
-      - libffi-dev
-      - libssl-dev
-      - rustc
-      - cargo
-```
-
-### Provider charm
-The provider charm is the charm providing certificates to another charm that requires them. In
-this example, the provider charm is storing its private key using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateCreationRequestEvent,
-    CertificateRevocationRequestEvent,
-    TLSCertificatesProvidesV1,
-    generate_private_key,
-)
-from ops.charm import CharmBase, InstallEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-def generate_ca(private_key: bytes, subject: str) -> str:
-    return "whatever ca content"
-
-
-def generate_certificate(ca: str, private_key: str, csr: str) -> str:
-    return "Whatever certificate"
-
-
-class ExampleProviderCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.certificates = TLSCertificatesProvidesV1(self, "certificates")
-        self.framework.observe(
-            self.certificates.on.certificate_request, self._on_certificate_request
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revocation_request
-        )
-        self.framework.observe(self.on.install, self._on_install)
-
-    def _on_install(self, event: InstallEvent) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        ca_certificate = generate_ca(private_key=private_key, subject="whatever")
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {
-                "private_key_password": "banana",
-                "private_key": private_key,
-                "ca_certificate": ca_certificate,
-            }
-        )
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        certificate = generate_certificate(
-            ca=ca_certificate,
-            private_key=private_key,
-            csr=event.certificate_signing_request,
-        )
-
-        self.certificates.set_relation_certificate(
-            certificate=certificate,
-            certificate_signing_request=event.certificate_signing_request,
-            ca=ca_certificate,
-            chain=[ca_certificate, certificate],
-            relation_id=event.relation_id,
-        )
-
-    def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
-        # Do what you want to do with this information
-        pass
-
-
-if __name__ == "__main__":
-    main(ExampleProviderCharm)
-```
-
-### Requirer charm
-The requirer charm is the charm requiring certificates from another charm that provides them. In
-this example, the requirer charm is storing its certificates using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateAvailableEvent,
-    CertificateExpiringEvent,
-    TLSCertificatesRequiresV1,
-    generate_csr,
-    generate_private_key,
-)
-from ops.charm import CharmBase, RelationJoinedEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-class ExampleRequirerCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.cert_subject = "whatever"
-        self.certificates = TLSCertificatesRequiresV1(self, "certificates")
-        self.framework.observe(self.on.install, self._on_install)
-        self.framework.observe(
-            self.on.certificates_relation_joined, self._on_certificates_relation_joined
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_available, self._on_certificate_available
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_expiring, self._on_certificate_expiring
-        )
-
-    def _on_install(self, event) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {"private_key_password": "banana", "private_key": private_key.decode()}
-        )
-
-    def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        replicas_relation.data[self.app].update({"csr": csr.decode()})
-        self.certificates.request_certificate_creation(certificate_signing_request=csr)
-
-    def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update({"certificate": event.certificate})
-        replicas_relation.data[self.app].update({"ca": event.ca})
-        replicas_relation.data[self.app].update({"chain": event.chain})
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-
-
-if __name__ == "__main__":
-    main(ExampleRequirerCharm)
-```
-"""  # noqa: D405, D410, D411, D214, D416
-
-import copy
-import json
-import logging
-import uuid
-from datetime import datetime, timedelta
-from ipaddress import IPv4Address
-from typing import Dict, List, Optional
-
-from cryptography import x509
-from cryptography.hazmat._oid import ExtensionOID
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.x509.extensions import Extension, ExtensionNotFound
-from jsonschema import exceptions, validate  # type: ignore[import]
-from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent
-from ops.framework import EventBase, EventSource, Handle, Object
-
-# The unique Charmhub library identifier, never change it
-LIBID = "afd8c2bccf834997afce12c2706d2ede"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 10
-
-REQUIRER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` requirer root schema",
-    "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificate_signing_requests": [
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-            ]
-        }
-    ],
-    "properties": {
-        "certificate_signing_requests": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {"certificate_signing_request": {"type": "string"}},
-                "required": ["certificate_signing_request"],
-            },
-        }
-    },
-    "required": ["certificate_signing_requests"],
-    "additionalProperties": True,
-}
-
-PROVIDER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` provider root schema",
-    "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.",  # noqa: E501
-    "example": [
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                }
-            ]
-        }
-    ],
-    "properties": {
-        "certificates": {
-            "$id": "#/properties/certificates",
-            "type": "array",
-            "items": {
-                "$id": "#/properties/certificates/items",
-                "type": "object",
-                "required": ["certificate_signing_request", "certificate", "ca", "chain"],
-                "properties": {
-                    "certificate_signing_request": {
-                        "$id": "#/properties/certificates/items/certificate_signing_request",
-                        "type": "string",
-                    },
-                    "certificate": {
-                        "$id": "#/properties/certificates/items/certificate",
-                        "type": "string",
-                    },
-                    "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
-                    "chain": {
-                        "$id": "#/properties/certificates/items/chain",
-                        "type": "array",
-                        "items": {
-                            "type": "string",
-                            "$id": "#/properties/certificates/items/chain/items",
-                        },
-                    },
-                },
-                "additionalProperties": True,
-            },
-        }
-    },
-    "required": ["certificates"],
-    "additionalProperties": True,
-}
-
-
-logger = logging.getLogger(__name__)
-
-
-class CertificateAvailableEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is available."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-class CertificateExpiringEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is almost expired."""
-
-    def __init__(self, handle, certificate: str, expiry: str):
-        """CertificateExpiringEvent.
-
-        Args:
-            handle (Handle): Juju framework handle
-            certificate (str): TLS Certificate
-            expiry (str): Datetime string reprensenting the time at which the certificate
-                won't be valid anymore.
-        """
-        super().__init__(handle)
-        self.certificate = certificate
-        self.expiry = expiry
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate, "expiry": self.expiry}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.expiry = snapshot["expiry"]
-
-
-class CertificateExpiredEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is expired."""
-
-    def __init__(self, handle: Handle, certificate: str):
-        super().__init__(handle)
-        self.certificate = certificate
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-
-
-class CertificateCreationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is required."""
-
-    def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int):
-        super().__init__(handle)
-        self.certificate_signing_request = certificate_signing_request
-        self.relation_id = relation_id
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate_signing_request": self.certificate_signing_request,
-            "relation_id": self.relation_id,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.relation_id = snapshot["relation_id"]
-
-
-class CertificateRevocationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate needs to be revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: str,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-def _load_relation_data(raw_relation_data: dict) -> dict:
-    """Loads relation data from the relation data bag.
-
-    Json loads all data.
-
-    Args:
-        raw_relation_data: Relation data from the databag
-
-    Returns:
-        dict: Relation data in dict format.
-    """
-    certificate_data = dict()
-    for key in raw_relation_data:
-        try:
-            certificate_data[key] = json.loads(raw_relation_data[key])
-        except (json.decoder.JSONDecodeError, TypeError):
-            certificate_data[key] = raw_relation_data[key]
-    return certificate_data
-
-
-def generate_ca(
-    private_key: bytes,
-    subject: str,
-    private_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    country: str = "US",
-) -> bytes:
-    """Generates a CA Certificate.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): Certificate subject
-        private_key_password (bytes): Private key password
-        validity (int): Certificate validity time (in days)
-        country (str): Certificate Issuing country
-
-    Returns:
-        bytes: CA Certificate.
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    subject = issuer = x509.Name(
-        [
-            x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
-            x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
-        ]
-    )
-    subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
-        private_key_object.public_key()  # type: ignore[arg-type]
-    )
-    subject_identifier = key_identifier = subject_identifier_object.public_bytes()
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(private_key_object.public_key())  # type: ignore[arg-type]
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-        .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
-        .add_extension(
-            x509.AuthorityKeyIdentifier(
-                key_identifier=key_identifier,
-                authority_cert_issuer=None,
-                authority_cert_serial_number=None,
-            ),
-            critical=False,
-        )
-        .add_extension(
-            x509.BasicConstraints(ca=True, path_length=None),
-            critical=True,
-        )
-        .sign(private_key_object, hashes.SHA256())  # type: ignore[arg-type]
-    )
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_certificate(
-    csr: bytes,
-    ca: bytes,
-    ca_key: bytes,
-    ca_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    alt_names: List[str] = None,
-) -> bytes:
-    """Generates a TLS certificate based on a CSR.
-
-    Args:
-        csr (bytes): CSR
-        ca (bytes): CA Certificate
-        ca_key (bytes): CA private key
-        ca_key_password: CA private key password
-        validity (int): Certificate validity (in days)
-        alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
-
-    Returns:
-        bytes: Certificate
-    """
-    csr_object = x509.load_pem_x509_csr(csr)
-    subject = csr_object.subject
-    issuer = x509.load_pem_x509_certificate(ca).issuer
-    private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
-
-    certificate_builder = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(csr_object.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-    )
-
-    extensions_list = csr_object.extensions
-    san_ext: Optional[x509.Extension] = None
-    if alt_names:
-        full_sans_dns = alt_names.copy()
-        try:
-            loaded_san_ext = csr_object.extensions.get_extension_for_class(
-                x509.SubjectAlternativeName
-            )
-            full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName))
-        except ExtensionNotFound:
-            pass
-        finally:
-            san_ext = Extension(
-                ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
-                False,
-                x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]),
-            )
-            if not extensions_list:
-                extensions_list = x509.Extensions([san_ext])
-
-    for extension in extensions_list:
-        if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext:
-            extension = san_ext
-
-        certificate_builder = certificate_builder.add_extension(
-            extension.value,
-            critical=extension.critical,
-        )
-    certificate_builder._version = x509.Version.v3
-    cert = certificate_builder.sign(private_key, hashes.SHA256())  # type: ignore[arg-type]
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_pfx_package(
-    certificate: bytes,
-    private_key: bytes,
-    package_password: str,
-    private_key_password: Optional[bytes] = None,
-) -> bytes:
-    """Generates a PFX package to contain the TLS certificate and private key.
-
-    Args:
-        certificate (bytes): TLS certificate
-        private_key (bytes): Private key
-        package_password (str): Password to open the PFX package
-        private_key_password (bytes): Private key password
-
-    Returns:
-        bytes:
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    certificate_object = x509.load_pem_x509_certificate(certificate)
-    name = certificate_object.subject.rfc4514_string()
-    pfx_bytes = pkcs12.serialize_key_and_certificates(
-        name=name.encode(),
-        cert=certificate_object,
-        key=private_key_object,  # type: ignore[arg-type]
-        cas=None,
-        encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()),
-    )
-    return pfx_bytes
-
-
-def generate_private_key(
-    password: Optional[bytes] = None,
-    key_size: int = 2048,
-    public_exponent: int = 65537,
-) -> bytes:
-    """Generates a private key.
-
-    Args:
-        password (bytes): Password for decrypting the private key
-        key_size (int): Key size in bytes
-        public_exponent: Public exponent.
-
-    Returns:
-        bytes: Private Key
-    """
-    private_key = rsa.generate_private_key(
-        public_exponent=public_exponent,
-        key_size=key_size,
-    )
-    key_bytes = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.TraditionalOpenSSL,
-        encryption_algorithm=serialization.BestAvailableEncryption(password)
-        if password
-        else serialization.NoEncryption(),
-    )
-    return key_bytes
-
-
-def generate_csr(
-    private_key: bytes,
-    subject: str,
-    add_unique_id_to_subject_name: bool = True,
-    organization: str = None,
-    email_address: str = None,
-    country_name: str = None,
-    private_key_password: Optional[bytes] = None,
-    sans: Optional[List[str]] = None,
-    sans_oid: Optional[List[str]] = None,
-    sans_ip: Optional[List[str]] = None,
-    sans_dns: Optional[List[str]] = None,
-    additional_critical_extensions: Optional[List] = None,
-) -> bytes:
-    """Generates a CSR using private key and subject.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): CSR Subject.
-        add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
-            subject name. Always leave to "True" when the CSR is used to request certificates
-            using the tls-certificates relation.
-        organization (str): Name of organization.
-        email_address (str): Email address.
-        country_name (str): Country Name.
-        private_key_password (bytes): Private key password
-        sans (list): Use sans_dns - this will be deprecated in a future release
-            List of DNS subject alternative names (keeping it for now for backward compatibility)
-        sans_oid (list): List of registered ID SANs
-        sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
-        sans_ip (list): List of IP subject alternative names
-        additional_critical_extensions (list): List if critical additional extension objects.
-            Object must be a x509 ExtensionType.
-
-    Returns:
-        bytes: CSR
-    """
-    signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
-    subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
-    if add_unique_id_to_subject_name:
-        unique_identifier = uuid.uuid4()
-        subject_name.append(
-            x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
-        )
-    if organization:
-        subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
-    if email_address:
-        subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
-    if country_name:
-        subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
-    csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
-
-    _sans: List[x509.GeneralName] = []
-    if sans_oid:
-        _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
-    if sans_ip:
-        _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
-    if sans:
-        _sans.extend([x509.DNSName(san) for san in sans])
-    if sans_dns:
-        _sans.extend([x509.DNSName(san) for san in sans_dns])
-    if _sans:
-        csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
-
-    if additional_critical_extensions:
-        for extension in additional_critical_extensions:
-            csr = csr.add_extension(extension, critical=True)
-
-    signed_certificate = csr.sign(signing_key, hashes.SHA256())  # type: ignore[arg-type]
-    return signed_certificate.public_bytes(serialization.Encoding.PEM)
-
-
-class CertificatesProviderCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates provider charm can leverage."""
-
-    certificate_creation_request = EventSource(CertificateCreationRequestEvent)
-    certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
-
-
-class CertificatesRequirerCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates requirer charm can leverage."""
-
-    certificate_available = EventSource(CertificateAvailableEvent)
-    certificate_expiring = EventSource(CertificateExpiringEvent)
-    certificate_expired = EventSource(CertificateExpiredEvent)
-
-
-class TLSCertificatesProvidesV1(Object):
-    """TLS certificates provider class to be instantiated by TLS certificates providers."""
-
-    on = CertificatesProviderCharmEvents()
-
-    def __init__(self, charm: CharmBase, relationship_name: str):
-        super().__init__(charm, relationship_name)
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.charm = charm
-        self.relationship_name = relationship_name
-
-    def _add_certificate(
-        self,
-        relation_id: int,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ) -> None:
-        """Adds certificate to relation data.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate Signing Request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_certificate = {
-            "certificate": certificate,
-            "certificate_signing_request": certificate_signing_request,
-            "ca": ca,
-            "chain": chain,
-        }
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        if new_certificate in certificates:
-            logger.info("Certificate already in relation data - Doing nothing")
-            return
-        certificates.append(new_certificate)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    def _remove_certificate(
-        self,
-        relation_id: int,
-        certificate: str = None,
-        certificate_signing_request: str = None,
-    ) -> None:
-        """Removes certificate from a given relation based on user provided certificate or csr.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate (optional)
-            certificate_signing_request: Certificate signing request (optional)
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name,
-            relation_id=relation_id,
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
-            )
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        for certificate_dict in certificates:
-            if certificate and certificate_dict["certificate"] == certificate:
-                certificates.remove(certificate_dict)
-            if (
-                certificate_signing_request
-                and certificate_dict["certificate_signing_request"] == certificate_signing_request
-            ):
-                certificates.remove(certificate_dict)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Uses JSON schema validator to validate relation data content.
-
-        Args:
-            certificates_data (dict): Certificate data dictionary as retrieved from relation data.
-
-        Returns:
-            bool: True/False depending on whether the relation data follows the json schema.
-        """
-        try:
-            validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def revoke_all_certificates(self) -> None:
-        """Revokes all certificates of this provider.
-
-        This method is meant to be used when the Root CA has changed.
-        """
-        for relation in self.model.relations[self.relationship_name]:
-            relation.data[self.model.app]["certificates"] = json.dumps([])
-
-    def set_relation_certificate(
-        self,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        relation_id: int,
-    ) -> None:
-        """Adds certificates to relation data.
-
-        Args:
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate signing request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-            relation_id (int): Juju relation ID
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        self._remove_certificate(
-            certificate_signing_request=certificate_signing_request.strip(),
-            relation_id=relation_id,
-        )
-        self._add_certificate(
-            relation_id=relation_id,
-            certificate=certificate.strip(),
-            certificate_signing_request=certificate_signing_request.strip(),
-            ca=ca.strip(),
-            chain=[cert.strip() for cert in chain],
-        )
-
-    def remove_certificate(self, certificate: str) -> None:
-        """Removes a given certificate from relation data.
-
-        Args:
-            certificate (str): TLS Certificate
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.relations[self.relationship_name]
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        for certificate_relation in certificates_relation:
-            self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed event.
-
-        Looks at the relation data and either emits:
-        - certificate request event: If the unit relation data contains a CSR for which
-            a certificate does not exist in the provider relation data.
-        - certificate revocation event: If the provider relation data contains a CSR for which
-            a csr does not exist in the requirer relation data.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        assert event.unit is not None
-        requirer_relation_data = _load_relation_data(event.relation.data[event.unit])
-        provider_relation_data = _load_relation_data(event.relation.data[self.charm.app])
-        if not self._relation_data_is_valid(requirer_relation_data):
-            logger.warning(
-                f"Relation data did not pass JSON Schema validation: {requirer_relation_data}"
-            )
-            return
-        provider_certificates = provider_relation_data.get("certificates", [])
-        requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-        provider_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in provider_certificates
-        ]
-        requirer_unit_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in requirer_csrs
-        ]
-        for certificate_signing_request in requirer_unit_csrs:
-            if certificate_signing_request not in provider_csrs:
-                self.on.certificate_creation_request.emit(
-                    certificate_signing_request=certificate_signing_request,
-                    relation_id=event.relation.id,
-                )
-        self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
-
-    def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
-        """Revokes certificates for which no unit has a CSR.
-
-        Goes through all generated certificates and compare agains the list of CSRS for all units
-        of a given relationship.
-
-        Args:
-            relation_id (int): Relation id
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app])
-        list_of_csrs: List[str] = []
-        for unit in certificates_relation.units:
-            requirer_relation_data = _load_relation_data(certificates_relation.data[unit])
-            requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-            list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs)
-        provider_certificates = provider_relation_data.get("certificates", [])
-        for certificate in provider_certificates:
-            if certificate["certificate_signing_request"] not in list_of_csrs:
-                self.on.certificate_revocation_request.emit(
-                    certificate=certificate["certificate"],
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-                self.remove_certificate(certificate=certificate["certificate"])
-
-
-class TLSCertificatesRequiresV1(Object):
-    """TLS certificates requirer class to be instantiated by TLS certificates requirers."""
-
-    on = CertificatesRequirerCharmEvents()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relationship_name: str,
-        expiry_notification_time: int = 168,
-    ):
-        """Generates/use private key and observes relation changed event.
-
-        Args:
-            charm: Charm object
-            relationship_name: Juju relation name
-            expiry_notification_time (int): Time difference between now and expiry (in hours).
-                Used to trigger the CertificateExpiring event. Default: 7 days.
-        """
-        super().__init__(charm, relationship_name)
-        self.relationship_name = relationship_name
-        self.charm = charm
-        self.expiry_notification_time = expiry_notification_time
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(charm.on.update_status, self._on_update_status)
-
-    @property
-    def _requirer_csrs(self) -> List[Dict[str, str]]:
-        """Returns list of requirer CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
-        return requirer_relation_data.get("certificate_signing_requests", [])
-
-    @property
-    def _provider_certificates(self) -> List[Dict[str, str]]:
-        """Returns list of provider CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        if not relation.app:
-            raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        return provider_relation_data.get("certificates", [])
-
-    def _add_requirer_csr(self, csr: str) -> None:
-        """Adds CSR to relation data.
-
-        Args:
-            csr (str): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_csr_dict = {"certificate_signing_request": csr}
-        if new_csr_dict in self._requirer_csrs:
-            logger.info("CSR already in relation data - Doing nothing")
-            return
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        requirer_csrs.append(new_csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def _remove_requirer_csr(self, csr: str) -> None:
-        """Removes CSR from relation data.
-
-        Args:
-            csr (str): Certificate signing request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        csr_dict = {"certificate_signing_request": csr}
-        if csr_dict not in requirer_csrs:
-            logger.info("CSR not in relation data - Doing nothing")
-            return
-        requirer_csrs.remove(csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def request_certificate_creation(self, certificate_signing_request: bytes) -> None:
-        """Request TLS certificate to provider charm.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            message = (
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-            logger.error(message)
-            raise RuntimeError(message)
-        self._add_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate request sent to provider")
-
-    def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
-        """Removes CSR from relation data.
-
-        The provider of this relation is then expected to remove certificates associated to this
-        CSR from the relation data as well and emit a request_certificate_revocation event for the
-        provider charm to interpret.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        self._remove_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate revocation sent to provider")
-
-    def request_certificate_renewal(
-        self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
-    ) -> None:
-        """Renews certificate.
-
-        Removes old CSR from relation data and adds new one.
-
-        Args:
-            old_certificate_signing_request: Old CSR
-            new_certificate_signing_request: New CSR
-
-        Returns:
-            None
-        """
-        try:
-            self.request_certificate_revocation(
-                certificate_signing_request=old_certificate_signing_request
-            )
-        except RuntimeError:
-            logger.warning("Certificate revocation failed.")
-        self.request_certificate_creation(
-            certificate_signing_request=new_certificate_signing_request
-        )
-        logger.info("Certificate renewal request completed.")
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Checks whether relation data is valid based on json schema.
-
-        Args:
-            certificates_data: Certificate data in dict format.
-
-        Returns:
-            bool: Whether relation data is valid.
-        """
-        try:
-            validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed events.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{event.relation.data[relation.app]}"
-            )
-            return
-        requirer_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in self._requirer_csrs
-        ]
-        for certificate in self._provider_certificates:
-            if certificate["certificate_signing_request"] in requirer_csrs:
-                self.on.certificate_available.emit(
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    certificate=certificate["certificate"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Triggered on update status event.
-
-        Goes through each certificate in the "certificates" relation and checks their expiry date.
-        If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if
-        they are expired, emits a CertificateExpiredEvent.
-
-        Args:
-            event (UpdateStatusEvent): Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{relation.data[relation.app]}"
-            )
-            return
-        for certificate_dict in self._provider_certificates:
-            certificate = certificate_dict["certificate"]
-            try:
-                certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
-            except ValueError:
-                logger.warning("Could not load certificate.")
-                continue
-            time_difference = certificate_object.not_valid_after - datetime.utcnow()
-            if time_difference.total_seconds() < 0:
-                logger.warning("Certificate is expired")
-                self.on.certificate_expired.emit(certificate=certificate)
-                self.request_certificate_revocation(certificate.encode())
-                continue
-            if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60):
-                logger.warning("Certificate almost expired")
-                self.on.certificate_expiring.emit(
-                    certificate=certificate, expiry=certificate_object.not_valid_after.isoformat()
-                )
diff --git a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py b/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py
deleted file mode 100644
index e1769e8c..00000000
--- a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py
+++ /dev/null
@@ -1,558 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v1.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-
-import logging
-import socket
-import typing
-from typing import Any, Dict, Optional, Tuple, Union
-
-import yaml
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 5
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-
-try:
-    import jsonschema
-
-    DO_VALIDATION = True
-except ModuleNotFoundError:
-    log.warning(
-        "The `ingress` library needs the `jsonschema` package to be able "
-        "to do runtime data validation; without it, it will still work but validation "
-        "will be disabled. \n"
-        "It is recommended to add `jsonschema` to the 'requirements.txt' of your charm, "
-        "which will enable this feature."
-    )
-    DO_VALIDATION = False
-
-INGRESS_REQUIRES_APP_SCHEMA = {
-    "type": "object",
-    "properties": {
-        "model": {"type": "string"},
-        "name": {"type": "string"},
-        "host": {"type": "string"},
-        "port": {"type": "string"},
-        "strip-prefix": {"type": "string"},
-    },
-    "required": ["model", "name", "host", "port"],
-}
-
-INGRESS_PROVIDES_APP_SCHEMA = {
-    "type": "object",
-    "properties": {
-        "ingress": {"type": "object", "properties": {"url": {"type": "string"}}},
-    },
-    "required": ["ingress"],
-}
-
-try:
-    from typing import TypedDict
-except ImportError:
-    from typing_extensions import TypedDict  # py35 compat
-
-# Model of the data a unit implementing the requirer will need to provide.
-RequirerData = TypedDict(
-    "RequirerData",
-    {"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool},
-    total=False,
-)
-# Provider ingress data model.
-ProviderIngressData = TypedDict("ProviderIngressData", {"url": str})
-# Provider application databag model.
-ProviderApplicationData = TypedDict("ProviderApplicationData", {"ingress": ProviderIngressData})
-
-
-def _validate_data(data, schema):
-    """Checks whether `data` matches `schema`.
-
-    Will raise DataValidationError if the data is not valid, else return None.
-    """
-    if not DO_VALIDATION:
-        return
-    try:
-        jsonschema.validate(instance=data, schema=schema)
-    except jsonschema.ValidationError as e:
-        raise DataValidationError(data, schema) from e
-
-
-class DataValidationError(RuntimeError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__ = ()  # type: Tuple[str, ...]
-    __optional_kwargs__ = {}  # type: Dict[str, Any]
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self) -> dict:
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot: dict) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "port", "host", "strip_prefix")
-
-    if typing.TYPE_CHECKING:
-        name = None  # type: str
-        model = None  # type: str
-        port = None  # type: int
-        host = None  # type: str
-        strip_prefix = False  # type: bool
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self._get_requirer_data(event.relation)
-            self.on.data_provided.emit(
-                event.relation,
-                data["name"],
-                data["model"],
-                data["port"],
-                data["host"],
-                data.get("strip-prefix", False),
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_data(self, relation: Relation) -> RequirerData:
-        """Fetch and validate the requirer's app databag.
-
-        For convenience, we convert 'port' to integer.
-        """
-        if not all((relation.app, relation.app.name)):
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return {}
-
-        databag = relation.data[relation.app]
-        remote_data = {}  # type: Dict[str, Union[int, str]]
-        for k in ("port", "host", "model", "name", "mode", "strip-prefix"):
-            v = databag.get(k)
-            if v is not None:
-                remote_data[k] = v
-        _validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA)
-        remote_data["port"] = int(remote_data["port"])
-        remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False))
-        return remote_data
-
-    def get_data(self, relation: Relation) -> RequirerData:
-        """Fetch the remote app's databag, i.e. the requirer data."""
-        return self._get_requirer_data(relation)
-
-    def is_ready(self, relation: Relation = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            return bool(self._get_requirer_data(relation))
-        except DataValidationError as e:
-            log.warning("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _provided_url(self, relation: Relation) -> ProviderIngressData:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not all((relation.app, relation.app.name, self.unit.is_leader())):
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return {}  # noqa
-
-        # fetch the provider's app databag
-        raw_data = relation.data[self.app].get("ingress")
-        if not raw_data:
-            raise RuntimeError("This application did not `publish_url` yet.")
-
-        ingress: ProviderIngressData = yaml.safe_load(raw_data)
-        _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA)
-        return ingress
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress = {"url": url}
-        ingress_data = {"ingress": ingress}
-        _validate_data(ingress_data, INGRESS_PROVIDES_APP_SCHEMA)
-        relation.data[self.app]["ingress"] = yaml.safe_dump(ingress)
-
-    @property
-    def proxied_endpoints(self):
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            results[ingress_relation.app.name] = self._provided_url(ingress_relation)
-
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url = None  # type: str
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()
-    # used to prevent spur1ious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: str = None,
-        port: int = None,
-        strip_prefix: bool = False,
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-
-        self._stored.set_default(current_url=None)
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data(event.relation)
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:
-                self._stored.current_url = new_url
-                self.on.ready.emit(event.relation, new_url)
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None
-        self.on.revoked.emit(event.relation)
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        for relation in self.relations:
-            self._publish_auto_data(relation)
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.warning("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self, relation: Relation):
-        if self._auto_data and self.unit.is_leader():
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(self, *, host: str = None, port: int):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        NB only the leader unit is supposed to do this.
-
-        Args:
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        # get only the leader to publish the data since we only
-        # require one unit to publish it -- it will not differ between units,
-        # unlike in ingress-per-unit.
-        assert self.unit.is_leader(), "only leaders should do this."
-        assert self.relation, "no relation"
-
-        if not host:
-            host = socket.getfqdn()
-
-        data = {
-            "model": self.model.name,
-            "name": self.app.name,
-            "host": host,
-            "port": str(port),
-        }
-
-        if self._strip_prefix:
-            data["strip-prefix"] = "true"
-
-        _validate_data(data, INGRESS_REQUIRES_APP_SCHEMA)
-        self.relation.data[self.app].update(data)
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            raw = relation.data.get(relation.app, {}).get("ingress")
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not raw:
-            return None
-
-        ingress: ProviderIngressData = yaml.safe_load(raw)
-        _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA)
-        return ingress["url"]
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = self._stored.current_url or None  # type: ignore
-        assert isinstance(data, (str, type(None)))  # for static checker
-        return data
diff --git a/charms/openstack-hypervisor/osci.yaml b/charms/openstack-hypervisor/osci.yaml
deleted file mode 100644
index 91848efd..00000000
--- a/charms/openstack-hypervisor/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: openstack-hypervisor
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/openstack-hypervisor/pyproject.toml b/charms/openstack-hypervisor/pyproject.toml
deleted file mode 100644
index 30821404..00000000
--- a/charms/openstack-hypervisor/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/openstack-hypervisor/rename.sh b/charms/openstack-hypervisor/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/openstack-hypervisor/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/openstack-hypervisor/requirements.txt b/charms/openstack-hypervisor/requirements.txt
index 17f89a30..978f759e 100644
--- a/charms/openstack-hypervisor/requirements.txt
+++ b/charms/openstack-hypervisor/requirements.txt
@@ -7,8 +7,9 @@ jinja2
 cosl==0.0.5 ; python_version >= "3.8"
 pydantic==1.10.12 ; python_version >= "3.8"
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
-
 # This charm does not use lightkube* but ops_sunbeam requires it atm
 lightkube
 lightkube-models
+
+# From ops_sunbeam
+tenacity
diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py
index d77370a7..d8ad6b9f 100755
--- a/charms/openstack-hypervisor/src/charm.py
+++ b/charms/openstack-hypervisor/src/charm.py
@@ -51,7 +51,6 @@ from ops.charm import (
 from ops.main import (
     main,
 )
-
 from utils import (
     get_local_ip_by_default_route,
 )
diff --git a/charms/openstack-hypervisor/test-requirements.txt b/charms/openstack-hypervisor/test-requirements.txt
deleted file mode 100644
index 0982ac98..00000000
--- a/charms/openstack-hypervisor/test-requirements.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-ops
-cosl==0.0.5 ; python_version >= "3.8"
-pydantic==1.10.12 ; python_version >= "3.8"
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py
index 9b89b138..dbaba261 100644
--- a/charms/openstack-hypervisor/tests/unit/test_charm.py
+++ b/charms/openstack-hypervisor/tests/unit/test_charm.py
@@ -20,9 +20,8 @@ from unittest import (
     mock,
 )
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _HypervisorOperatorCharm(charm.HypervisorOperatorCharm):
diff --git a/charms/openstack-hypervisor/tox.ini b/charms/openstack-hypervisor/tox.ini
deleted file mode 100644
index 38807328..00000000
--- a/charms/openstack-hypervisor/tox.ini
+++ /dev/null
@@ -1,168 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/ovn-central-k8s/.flake8 b/charms/ovn-central-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/ovn-central-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/ovn-central-k8s/.gitignore b/charms/ovn-central-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/ovn-central-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/ovn-central-k8s/.gitreview b/charms/ovn-central-k8s/.gitreview
deleted file mode 100644
index 95d816b4..00000000
--- a/charms/ovn-central-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=x/charm-ovn-central-k8s.git
-defaultbranch=main
diff --git a/charms/ovn-central-k8s/.jujuignore b/charms/ovn-central-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/ovn-central-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/ovn-central-k8s/.stestr.conf b/charms/ovn-central-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/ovn-central-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/ovn-central-k8s/.zuul.yaml b/charms/ovn-central-k8s/.zuul.yaml
deleted file mode 100644
index 0a41ba84..00000000
--- a/charms/ovn-central-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: ovn-central-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/ovn-central-k8s/charmcraft.yaml b/charms/ovn-central-k8s/charmcraft.yaml
index eea1df04..e67d37b5 100644
--- a/charms/ovn-central-k8s/charmcraft.yaml
+++ b/charms/ovn-central-k8s/charmcraft.yaml
@@ -29,4 +29,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/ovn-central-k8s/fetch-libs.sh b/charms/ovn-central-k8s/fetch-libs.sh
deleted file mode 100755
index 2b40c7b4..00000000
--- a/charms/ovn-central-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
diff --git a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
deleted file mode 100644
index 732679a6..00000000
--- a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""TODO: Add a proper docstring here.
-
-This is a placeholder docstring for this charm library. Docstrings are
-presented on Charmhub and updated whenever you push a new version of the
-library.
-
-Complete documentation about creating and documenting libraries can be found
-in the SDK docs at https://juju.is/docs/sdk/libraries.
-
-See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
-share and consume charm libraries. They serve to enhance collaboration
-between charmers. Use a charmer's libraries for classes that handle
-integration with their charm.
-
-Bear in mind that new revisions of the different major API versions (v0, v1,
-v2 etc) are maintained independently.  You can continue to update v0 and v1
-after you have pushed v3.
-
-Markdown is supported, following the CommonMark specification.
-"""
-
-import logging
-import typing
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "114b7bb1970445daa61650e451f9da62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-# TODO: add your code here! Happy coding!
-class OVSDBCMSConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSConnectedEvent)
-    ready = EventSource(OVSDBCMSReadyEvent)
-    goneaway = EventSource(OVSDBCMSGoneAwayEvent)
-
-
-class OVSDBCMSRequires(Object):
-    """
-    OVSDBCMSRequires class
-    """
-
-    on = OVSDBCMSServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """OVSDBCMS relation joined."""
-        logging.debug("OVSDBCMSRequires on_joined")
-        self.on.connected.emit()
-
-    def bound_hostnames(self):
-        return self.get_all_unit_values("bound-hostname")
-
-    def bound_addresses(self):
-        return self.get_all_unit_values("bound-address")
-
-    def remote_ready(self):
-        return all(self.bound_hostnames()) or all(self.bound_addresses())
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """OVSDBCMS relation changed."""
-        logging.debug("OVSDBCMSRequires on_changed")
-        if self.remote_ready():
-            self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """OVSDBCMS relation broken."""
-        logging.debug("OVSDBCMSRequires on_broken")
-        self.on.goneaway.emit()
-
-    def get_all_unit_values(self, key: str) -> typing.List[str]:
-        """Retrieve value for key from all related units."""
-        values = []
-        relation = self.framework.model.get_relation(self.relation_name)
-        if relation:
-            for unit in relation.units:
-                values.append(relation.data[unit].get(key))
-        return values
-
-
-
-class OVSDBCMSClientConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSClientReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSClientGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSClientConnectedEvent)
-    ready = EventSource(OVSDBCMSClientReadyEvent)
-    goneaway = EventSource(OVSDBCMSClientGoneAwayEvent)
-
-
-class OVSDBCMSProvides(Object):
-    """
-    OVSDBCMSProvides class
-    """
-
-    on = OVSDBCMSClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """Handle ovsdb-cms joined."""
-        logging.debug("OVSDBCMSProvides on_joined")
-        self.on.connected.emit()
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """Handle ovsdb-cms changed."""
-        logging.debug("OVSDBCMSProvides on_changed")
-        self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """Handle ovsdb-cms broken."""
-        logging.debug("OVSDBCMSProvides on_departed")
-        self.on.goneaway.emit()
-
-    def set_unit_data(self, settings: typing.Dict[str, str]) -> None:
-        """Publish settings on the peer unit data bag."""
-        relations = self.framework.model.relations[self.relation_name]
-        for relation in relations:
-            for k, v in settings.items():
-                relation.data[self.model.unit][k] = v
diff --git a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
deleted file mode 100644
index 1eda19bf..00000000
--- a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
+++ /dev/null
@@ -1,1261 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Library for the tls-certificates relation.
-
-This library contains the Requires and Provides classes for handling the tls-certificates
-interface.
-
-## Getting Started
-From a charm directory, fetch the library using `charmcraft`:
-
-```shell
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-```
-
-Add the following libraries to the charm's `requirements.txt` file:
-- jsonschema
-- cryptography
-
-Add the following section to the charm's `charmcraft.yaml` file:
-```yaml
-parts:
-  charm:
-    build-packages:
-      - libffi-dev
-      - libssl-dev
-      - rustc
-      - cargo
-```
-
-### Provider charm
-The provider charm is the charm providing certificates to another charm that requires them. In
-this example, the provider charm is storing its private key using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateCreationRequestEvent,
-    CertificateRevocationRequestEvent,
-    TLSCertificatesProvidesV1,
-    generate_private_key,
-)
-from ops.charm import CharmBase, InstallEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-def generate_ca(private_key: bytes, subject: str) -> str:
-    return "whatever ca content"
-
-
-def generate_certificate(ca: str, private_key: str, csr: str) -> str:
-    return "Whatever certificate"
-
-
-class ExampleProviderCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.certificates = TLSCertificatesProvidesV1(self, "certificates")
-        self.framework.observe(
-            self.certificates.on.certificate_request, self._on_certificate_request
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revocation_request
-        )
-        self.framework.observe(self.on.install, self._on_install)
-
-    def _on_install(self, event: InstallEvent) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        ca_certificate = generate_ca(private_key=private_key, subject="whatever")
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {
-                "private_key_password": "banana",
-                "private_key": private_key,
-                "ca_certificate": ca_certificate,
-            }
-        )
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        certificate = generate_certificate(
-            ca=ca_certificate,
-            private_key=private_key,
-            csr=event.certificate_signing_request,
-        )
-
-        self.certificates.set_relation_certificate(
-            certificate=certificate,
-            certificate_signing_request=event.certificate_signing_request,
-            ca=ca_certificate,
-            chain=[ca_certificate, certificate],
-            relation_id=event.relation_id,
-        )
-
-    def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
-        # Do what you want to do with this information
-        pass
-
-
-if __name__ == "__main__":
-    main(ExampleProviderCharm)
-```
-
-### Requirer charm
-The requirer charm is the charm requiring certificates from another charm that provides them. In
-this example, the requirer charm is storing its certificates using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateAvailableEvent,
-    CertificateExpiringEvent,
-    TLSCertificatesRequiresV1,
-    generate_csr,
-    generate_private_key,
-)
-from ops.charm import CharmBase, RelationJoinedEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-class ExampleRequirerCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.cert_subject = "whatever"
-        self.certificates = TLSCertificatesRequiresV1(self, "certificates")
-        self.framework.observe(self.on.install, self._on_install)
-        self.framework.observe(
-            self.on.certificates_relation_joined, self._on_certificates_relation_joined
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_available, self._on_certificate_available
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_expiring, self._on_certificate_expiring
-        )
-
-    def _on_install(self, event) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {"private_key_password": "banana", "private_key": private_key.decode()}
-        )
-
-    def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        replicas_relation.data[self.app].update({"csr": csr.decode()})
-        self.certificates.request_certificate_creation(certificate_signing_request=csr)
-
-    def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update({"certificate": event.certificate})
-        replicas_relation.data[self.app].update({"ca": event.ca})
-        replicas_relation.data[self.app].update({"chain": event.chain})
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-
-
-if __name__ == "__main__":
-    main(ExampleRequirerCharm)
-```
-"""  # noqa: D405, D410, D411, D214, D416
-
-import copy
-import json
-import logging
-import uuid
-from datetime import datetime, timedelta
-from ipaddress import IPv4Address
-from typing import Dict, List, Optional
-
-from cryptography import x509
-from cryptography.hazmat._oid import ExtensionOID
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.x509.extensions import Extension, ExtensionNotFound
-from jsonschema import exceptions, validate  # type: ignore[import]
-from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent
-from ops.framework import EventBase, EventSource, Handle, Object
-
-# The unique Charmhub library identifier, never change it
-LIBID = "afd8c2bccf834997afce12c2706d2ede"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 10
-
-REQUIRER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` requirer root schema",
-    "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificate_signing_requests": [
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-            ]
-        }
-    ],
-    "properties": {
-        "certificate_signing_requests": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {"certificate_signing_request": {"type": "string"}},
-                "required": ["certificate_signing_request"],
-            },
-        }
-    },
-    "required": ["certificate_signing_requests"],
-    "additionalProperties": True,
-}
-
-PROVIDER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` provider root schema",
-    "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.",  # noqa: E501
-    "example": [
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                }
-            ]
-        }
-    ],
-    "properties": {
-        "certificates": {
-            "$id": "#/properties/certificates",
-            "type": "array",
-            "items": {
-                "$id": "#/properties/certificates/items",
-                "type": "object",
-                "required": ["certificate_signing_request", "certificate", "ca", "chain"],
-                "properties": {
-                    "certificate_signing_request": {
-                        "$id": "#/properties/certificates/items/certificate_signing_request",
-                        "type": "string",
-                    },
-                    "certificate": {
-                        "$id": "#/properties/certificates/items/certificate",
-                        "type": "string",
-                    },
-                    "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
-                    "chain": {
-                        "$id": "#/properties/certificates/items/chain",
-                        "type": "array",
-                        "items": {
-                            "type": "string",
-                            "$id": "#/properties/certificates/items/chain/items",
-                        },
-                    },
-                },
-                "additionalProperties": True,
-            },
-        }
-    },
-    "required": ["certificates"],
-    "additionalProperties": True,
-}
-
-
-logger = logging.getLogger(__name__)
-
-
-class CertificateAvailableEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is available."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-class CertificateExpiringEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is almost expired."""
-
-    def __init__(self, handle, certificate: str, expiry: str):
-        """CertificateExpiringEvent.
-
-        Args:
-            handle (Handle): Juju framework handle
-            certificate (str): TLS Certificate
-            expiry (str): Datetime string reprensenting the time at which the certificate
-                won't be valid anymore.
-        """
-        super().__init__(handle)
-        self.certificate = certificate
-        self.expiry = expiry
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate, "expiry": self.expiry}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.expiry = snapshot["expiry"]
-
-
-class CertificateExpiredEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is expired."""
-
-    def __init__(self, handle: Handle, certificate: str):
-        super().__init__(handle)
-        self.certificate = certificate
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-
-
-class CertificateCreationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is required."""
-
-    def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int):
-        super().__init__(handle)
-        self.certificate_signing_request = certificate_signing_request
-        self.relation_id = relation_id
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate_signing_request": self.certificate_signing_request,
-            "relation_id": self.relation_id,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.relation_id = snapshot["relation_id"]
-
-
-class CertificateRevocationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate needs to be revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: str,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-def _load_relation_data(raw_relation_data: dict) -> dict:
-    """Loads relation data from the relation data bag.
-
-    Json loads all data.
-
-    Args:
-        raw_relation_data: Relation data from the databag
-
-    Returns:
-        dict: Relation data in dict format.
-    """
-    certificate_data = dict()
-    for key in raw_relation_data:
-        try:
-            certificate_data[key] = json.loads(raw_relation_data[key])
-        except (json.decoder.JSONDecodeError, TypeError):
-            certificate_data[key] = raw_relation_data[key]
-    return certificate_data
-
-
-def generate_ca(
-    private_key: bytes,
-    subject: str,
-    private_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    country: str = "US",
-) -> bytes:
-    """Generates a CA Certificate.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): Certificate subject
-        private_key_password (bytes): Private key password
-        validity (int): Certificate validity time (in days)
-        country (str): Certificate Issuing country
-
-    Returns:
-        bytes: CA Certificate.
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    subject = issuer = x509.Name(
-        [
-            x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
-            x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
-        ]
-    )
-    subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
-        private_key_object.public_key()  # type: ignore[arg-type]
-    )
-    subject_identifier = key_identifier = subject_identifier_object.public_bytes()
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(private_key_object.public_key())  # type: ignore[arg-type]
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-        .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
-        .add_extension(
-            x509.AuthorityKeyIdentifier(
-                key_identifier=key_identifier,
-                authority_cert_issuer=None,
-                authority_cert_serial_number=None,
-            ),
-            critical=False,
-        )
-        .add_extension(
-            x509.BasicConstraints(ca=True, path_length=None),
-            critical=True,
-        )
-        .sign(private_key_object, hashes.SHA256())  # type: ignore[arg-type]
-    )
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_certificate(
-    csr: bytes,
-    ca: bytes,
-    ca_key: bytes,
-    ca_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    alt_names: List[str] = None,
-) -> bytes:
-    """Generates a TLS certificate based on a CSR.
-
-    Args:
-        csr (bytes): CSR
-        ca (bytes): CA Certificate
-        ca_key (bytes): CA private key
-        ca_key_password: CA private key password
-        validity (int): Certificate validity (in days)
-        alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
-
-    Returns:
-        bytes: Certificate
-    """
-    csr_object = x509.load_pem_x509_csr(csr)
-    subject = csr_object.subject
-    issuer = x509.load_pem_x509_certificate(ca).issuer
-    private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
-
-    certificate_builder = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(csr_object.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-    )
-
-    extensions_list = csr_object.extensions
-    san_ext: Optional[x509.Extension] = None
-    if alt_names:
-        full_sans_dns = alt_names.copy()
-        try:
-            loaded_san_ext = csr_object.extensions.get_extension_for_class(
-                x509.SubjectAlternativeName
-            )
-            full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName))
-        except ExtensionNotFound:
-            pass
-        finally:
-            san_ext = Extension(
-                ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
-                False,
-                x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]),
-            )
-            if not extensions_list:
-                extensions_list = x509.Extensions([san_ext])
-
-    for extension in extensions_list:
-        if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext:
-            extension = san_ext
-
-        certificate_builder = certificate_builder.add_extension(
-            extension.value,
-            critical=extension.critical,
-        )
-    certificate_builder._version = x509.Version.v3
-    cert = certificate_builder.sign(private_key, hashes.SHA256())  # type: ignore[arg-type]
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_pfx_package(
-    certificate: bytes,
-    private_key: bytes,
-    package_password: str,
-    private_key_password: Optional[bytes] = None,
-) -> bytes:
-    """Generates a PFX package to contain the TLS certificate and private key.
-
-    Args:
-        certificate (bytes): TLS certificate
-        private_key (bytes): Private key
-        package_password (str): Password to open the PFX package
-        private_key_password (bytes): Private key password
-
-    Returns:
-        bytes:
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    certificate_object = x509.load_pem_x509_certificate(certificate)
-    name = certificate_object.subject.rfc4514_string()
-    pfx_bytes = pkcs12.serialize_key_and_certificates(
-        name=name.encode(),
-        cert=certificate_object,
-        key=private_key_object,  # type: ignore[arg-type]
-        cas=None,
-        encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()),
-    )
-    return pfx_bytes
-
-
-def generate_private_key(
-    password: Optional[bytes] = None,
-    key_size: int = 2048,
-    public_exponent: int = 65537,
-) -> bytes:
-    """Generates a private key.
-
-    Args:
-        password (bytes): Password for decrypting the private key
-        key_size (int): Key size in bytes
-        public_exponent: Public exponent.
-
-    Returns:
-        bytes: Private Key
-    """
-    private_key = rsa.generate_private_key(
-        public_exponent=public_exponent,
-        key_size=key_size,
-    )
-    key_bytes = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.TraditionalOpenSSL,
-        encryption_algorithm=serialization.BestAvailableEncryption(password)
-        if password
-        else serialization.NoEncryption(),
-    )
-    return key_bytes
-
-
-def generate_csr(
-    private_key: bytes,
-    subject: str,
-    add_unique_id_to_subject_name: bool = True,
-    organization: str = None,
-    email_address: str = None,
-    country_name: str = None,
-    private_key_password: Optional[bytes] = None,
-    sans: Optional[List[str]] = None,
-    sans_oid: Optional[List[str]] = None,
-    sans_ip: Optional[List[str]] = None,
-    sans_dns: Optional[List[str]] = None,
-    additional_critical_extensions: Optional[List] = None,
-) -> bytes:
-    """Generates a CSR using private key and subject.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): CSR Subject.
-        add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
-            subject name. Always leave to "True" when the CSR is used to request certificates
-            using the tls-certificates relation.
-        organization (str): Name of organization.
-        email_address (str): Email address.
-        country_name (str): Country Name.
-        private_key_password (bytes): Private key password
-        sans (list): Use sans_dns - this will be deprecated in a future release
-            List of DNS subject alternative names (keeping it for now for backward compatibility)
-        sans_oid (list): List of registered ID SANs
-        sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
-        sans_ip (list): List of IP subject alternative names
-        additional_critical_extensions (list): List if critical additional extension objects.
-            Object must be a x509 ExtensionType.
-
-    Returns:
-        bytes: CSR
-    """
-    signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
-    subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
-    if add_unique_id_to_subject_name:
-        unique_identifier = uuid.uuid4()
-        subject_name.append(
-            x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
-        )
-    if organization:
-        subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
-    if email_address:
-        subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
-    if country_name:
-        subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
-    csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
-
-    _sans: List[x509.GeneralName] = []
-    if sans_oid:
-        _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
-    if sans_ip:
-        _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
-    if sans:
-        _sans.extend([x509.DNSName(san) for san in sans])
-    if sans_dns:
-        _sans.extend([x509.DNSName(san) for san in sans_dns])
-    if _sans:
-        csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
-
-    if additional_critical_extensions:
-        for extension in additional_critical_extensions:
-            csr = csr.add_extension(extension, critical=True)
-
-    signed_certificate = csr.sign(signing_key, hashes.SHA256())  # type: ignore[arg-type]
-    return signed_certificate.public_bytes(serialization.Encoding.PEM)
-
-
-class CertificatesProviderCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates provider charm can leverage."""
-
-    certificate_creation_request = EventSource(CertificateCreationRequestEvent)
-    certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
-
-
-class CertificatesRequirerCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates requirer charm can leverage."""
-
-    certificate_available = EventSource(CertificateAvailableEvent)
-    certificate_expiring = EventSource(CertificateExpiringEvent)
-    certificate_expired = EventSource(CertificateExpiredEvent)
-
-
-class TLSCertificatesProvidesV1(Object):
-    """TLS certificates provider class to be instantiated by TLS certificates providers."""
-
-    on = CertificatesProviderCharmEvents()
-
-    def __init__(self, charm: CharmBase, relationship_name: str):
-        super().__init__(charm, relationship_name)
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.charm = charm
-        self.relationship_name = relationship_name
-
-    def _add_certificate(
-        self,
-        relation_id: int,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ) -> None:
-        """Adds certificate to relation data.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate Signing Request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_certificate = {
-            "certificate": certificate,
-            "certificate_signing_request": certificate_signing_request,
-            "ca": ca,
-            "chain": chain,
-        }
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        if new_certificate in certificates:
-            logger.info("Certificate already in relation data - Doing nothing")
-            return
-        certificates.append(new_certificate)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    def _remove_certificate(
-        self,
-        relation_id: int,
-        certificate: str = None,
-        certificate_signing_request: str = None,
-    ) -> None:
-        """Removes certificate from a given relation based on user provided certificate or csr.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate (optional)
-            certificate_signing_request: Certificate signing request (optional)
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name,
-            relation_id=relation_id,
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
-            )
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        for certificate_dict in certificates:
-            if certificate and certificate_dict["certificate"] == certificate:
-                certificates.remove(certificate_dict)
-            if (
-                certificate_signing_request
-                and certificate_dict["certificate_signing_request"] == certificate_signing_request
-            ):
-                certificates.remove(certificate_dict)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Uses JSON schema validator to validate relation data content.
-
-        Args:
-            certificates_data (dict): Certificate data dictionary as retrieved from relation data.
-
-        Returns:
-            bool: True/False depending on whether the relation data follows the json schema.
-        """
-        try:
-            validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def revoke_all_certificates(self) -> None:
-        """Revokes all certificates of this provider.
-
-        This method is meant to be used when the Root CA has changed.
-        """
-        for relation in self.model.relations[self.relationship_name]:
-            relation.data[self.model.app]["certificates"] = json.dumps([])
-
-    def set_relation_certificate(
-        self,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        relation_id: int,
-    ) -> None:
-        """Adds certificates to relation data.
-
-        Args:
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate signing request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-            relation_id (int): Juju relation ID
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        self._remove_certificate(
-            certificate_signing_request=certificate_signing_request.strip(),
-            relation_id=relation_id,
-        )
-        self._add_certificate(
-            relation_id=relation_id,
-            certificate=certificate.strip(),
-            certificate_signing_request=certificate_signing_request.strip(),
-            ca=ca.strip(),
-            chain=[cert.strip() for cert in chain],
-        )
-
-    def remove_certificate(self, certificate: str) -> None:
-        """Removes a given certificate from relation data.
-
-        Args:
-            certificate (str): TLS Certificate
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.relations[self.relationship_name]
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        for certificate_relation in certificates_relation:
-            self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed event.
-
-        Looks at the relation data and either emits:
-        - certificate request event: If the unit relation data contains a CSR for which
-            a certificate does not exist in the provider relation data.
-        - certificate revocation event: If the provider relation data contains a CSR for which
-            a csr does not exist in the requirer relation data.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        assert event.unit is not None
-        requirer_relation_data = _load_relation_data(event.relation.data[event.unit])
-        provider_relation_data = _load_relation_data(event.relation.data[self.charm.app])
-        if not self._relation_data_is_valid(requirer_relation_data):
-            logger.warning(
-                f"Relation data did not pass JSON Schema validation: {requirer_relation_data}"
-            )
-            return
-        provider_certificates = provider_relation_data.get("certificates", [])
-        requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-        provider_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in provider_certificates
-        ]
-        requirer_unit_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in requirer_csrs
-        ]
-        for certificate_signing_request in requirer_unit_csrs:
-            if certificate_signing_request not in provider_csrs:
-                self.on.certificate_creation_request.emit(
-                    certificate_signing_request=certificate_signing_request,
-                    relation_id=event.relation.id,
-                )
-        self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
-
-    def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
-        """Revokes certificates for which no unit has a CSR.
-
-        Goes through all generated certificates and compare agains the list of CSRS for all units
-        of a given relationship.
-
-        Args:
-            relation_id (int): Relation id
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app])
-        list_of_csrs: List[str] = []
-        for unit in certificates_relation.units:
-            requirer_relation_data = _load_relation_data(certificates_relation.data[unit])
-            requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-            list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs)
-        provider_certificates = provider_relation_data.get("certificates", [])
-        for certificate in provider_certificates:
-            if certificate["certificate_signing_request"] not in list_of_csrs:
-                self.on.certificate_revocation_request.emit(
-                    certificate=certificate["certificate"],
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-                self.remove_certificate(certificate=certificate["certificate"])
-
-
-class TLSCertificatesRequiresV1(Object):
-    """TLS certificates requirer class to be instantiated by TLS certificates requirers."""
-
-    on = CertificatesRequirerCharmEvents()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relationship_name: str,
-        expiry_notification_time: int = 168,
-    ):
-        """Generates/use private key and observes relation changed event.
-
-        Args:
-            charm: Charm object
-            relationship_name: Juju relation name
-            expiry_notification_time (int): Time difference between now and expiry (in hours).
-                Used to trigger the CertificateExpiring event. Default: 7 days.
-        """
-        super().__init__(charm, relationship_name)
-        self.relationship_name = relationship_name
-        self.charm = charm
-        self.expiry_notification_time = expiry_notification_time
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(charm.on.update_status, self._on_update_status)
-
-    @property
-    def _requirer_csrs(self) -> List[Dict[str, str]]:
-        """Returns list of requirer CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
-        return requirer_relation_data.get("certificate_signing_requests", [])
-
-    @property
-    def _provider_certificates(self) -> List[Dict[str, str]]:
-        """Returns list of provider CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        if not relation.app:
-            raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        return provider_relation_data.get("certificates", [])
-
-    def _add_requirer_csr(self, csr: str) -> None:
-        """Adds CSR to relation data.
-
-        Args:
-            csr (str): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_csr_dict = {"certificate_signing_request": csr}
-        if new_csr_dict in self._requirer_csrs:
-            logger.info("CSR already in relation data - Doing nothing")
-            return
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        requirer_csrs.append(new_csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def _remove_requirer_csr(self, csr: str) -> None:
-        """Removes CSR from relation data.
-
-        Args:
-            csr (str): Certificate signing request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        csr_dict = {"certificate_signing_request": csr}
-        if csr_dict not in requirer_csrs:
-            logger.info("CSR not in relation data - Doing nothing")
-            return
-        requirer_csrs.remove(csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def request_certificate_creation(self, certificate_signing_request: bytes) -> None:
-        """Request TLS certificate to provider charm.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            message = (
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-            logger.error(message)
-            raise RuntimeError(message)
-        self._add_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate request sent to provider")
-
-    def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
-        """Removes CSR from relation data.
-
-        The provider of this relation is then expected to remove certificates associated to this
-        CSR from the relation data as well and emit a request_certificate_revocation event for the
-        provider charm to interpret.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        self._remove_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate revocation sent to provider")
-
-    def request_certificate_renewal(
-        self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
-    ) -> None:
-        """Renews certificate.
-
-        Removes old CSR from relation data and adds new one.
-
-        Args:
-            old_certificate_signing_request: Old CSR
-            new_certificate_signing_request: New CSR
-
-        Returns:
-            None
-        """
-        try:
-            self.request_certificate_revocation(
-                certificate_signing_request=old_certificate_signing_request
-            )
-        except RuntimeError:
-            logger.warning("Certificate revocation failed.")
-        self.request_certificate_creation(
-            certificate_signing_request=new_certificate_signing_request
-        )
-        logger.info("Certificate renewal request completed.")
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Checks whether relation data is valid based on json schema.
-
-        Args:
-            certificates_data: Certificate data in dict format.
-
-        Returns:
-            bool: Whether relation data is valid.
-        """
-        try:
-            validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed events.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{event.relation.data[relation.app]}"
-            )
-            return
-        requirer_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in self._requirer_csrs
-        ]
-        for certificate in self._provider_certificates:
-            if certificate["certificate_signing_request"] in requirer_csrs:
-                self.on.certificate_available.emit(
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    certificate=certificate["certificate"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Triggered on update status event.
-
-        Goes through each certificate in the "certificates" relation and checks their expiry date.
-        If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if
-        they are expired, emits a CertificateExpiredEvent.
-
-        Args:
-            event (UpdateStatusEvent): Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{relation.data[relation.app]}"
-            )
-            return
-        for certificate_dict in self._provider_certificates:
-            certificate = certificate_dict["certificate"]
-            try:
-                certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
-            except ValueError:
-                logger.warning("Could not load certificate.")
-                continue
-            time_difference = certificate_object.not_valid_after - datetime.utcnow()
-            if time_difference.total_seconds() < 0:
-                logger.warning("Certificate is expired")
-                self.on.certificate_expired.emit(certificate=certificate)
-                self.request_certificate_revocation(certificate.encode())
-                continue
-            if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60):
-                logger.warning("Certificate almost expired")
-                self.on.certificate_expiring.emit(
-                    certificate=certificate, expiry=certificate_object.not_valid_after.isoformat()
-                )
diff --git a/charms/ovn-central-k8s/osci.yaml b/charms/ovn-central-k8s/osci.yaml
deleted file mode 100644
index 15e9a479..00000000
--- a/charms/ovn-central-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: ovn-central-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 23.09/edge
diff --git a/charms/ovn-central-k8s/pyproject.toml b/charms/ovn-central-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/ovn-central-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/ovn-central-k8s/rename.sh b/charms/ovn-central-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/ovn-central-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/ovn-central-k8s/requirements.txt b/charms/ovn-central-k8s/requirements.txt
index 9211d667..ddbedec1 100644
--- a/charms/ovn-central-k8s/requirements.txt
+++ b/charms/ovn-central-k8s/requirements.txt
@@ -11,4 +11,5 @@ lightkube
 lightkube-models
 ops
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+# From ops_sunbeam
+tenacity
diff --git a/charms/ovn-central-k8s/src/charm.py b/charms/ovn-central-k8s/src/charm.py
index ad0971ee..7158c4a7 100755
--- a/charms/ovn-central-k8s/src/charm.py
+++ b/charms/ovn-central-k8s/src/charm.py
@@ -35,6 +35,8 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts
 import ops_sunbeam.ovn.container_handlers as ovn_chandlers
 import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers
 import ops_sunbeam.relation_handlers as sunbeam_rhandlers
+import ovn
+import ovsdb as ch_ovsdb
 import tenacity
 from ops.framework import (
     StoredState,
@@ -43,9 +45,6 @@ from ops.main import (
     main,
 )
 
-import ovn
-import ovsdb as ch_ovsdb
-
 logger = logging.getLogger(__name__)
 
 OVN_SB_DB_CONTAINER = "ovn-sb-db-server"
diff --git a/charms/ovn-central-k8s/test-requirements.txt b/charms/ovn-central-k8s/test-requirements.txt
deleted file mode 100644
index da3b04b7..00000000
--- a/charms/ovn-central-k8s/test-requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py
index 2b4e5c08..ab676826 100644
--- a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py
+++ b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py
@@ -16,11 +16,10 @@
 
 """Tests for OVN central charm."""
 
+import charm
 import mock
 import ops_sunbeam.test_utils as test_utils
 
-import charm
-
 
 class _OVNCentralOperatorCharm(charm.OVNCentralOperatorCharm):
     def __init__(self, framework):
diff --git a/charms/ovn-central-k8s/tox.ini b/charms/ovn-central-k8s/tox.ini
deleted file mode 100644
index 2b8f98b1..00000000
--- a/charms/ovn-central-k8s/tox.ini
+++ /dev/null
@@ -1,169 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-  HOME
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/ovn-relay-k8s/.flake8 b/charms/ovn-relay-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/ovn-relay-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/ovn-relay-k8s/.gitignore b/charms/ovn-relay-k8s/.gitignore
deleted file mode 100644
index de9170b0..00000000
--- a/charms/ovn-relay-k8s/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
diff --git a/charms/ovn-relay-k8s/.gitreview b/charms/ovn-relay-k8s/.gitreview
deleted file mode 100644
index 5f989225..00000000
--- a/charms/ovn-relay-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=x/charm-ovn-relay-k8s.git
-defaultbranch=main
diff --git a/charms/ovn-relay-k8s/.jujuignore b/charms/ovn-relay-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/ovn-relay-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/ovn-relay-k8s/.stestr.conf b/charms/ovn-relay-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/ovn-relay-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/ovn-relay-k8s/.zuul.yaml b/charms/ovn-relay-k8s/.zuul.yaml
deleted file mode 100644
index 494e48c6..00000000
--- a/charms/ovn-relay-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-yoga-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: ovn-relay-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/ovn-relay-k8s/charmcraft.yaml b/charms/ovn-relay-k8s/charmcraft.yaml
index eea1df04..e67d37b5 100644
--- a/charms/ovn-relay-k8s/charmcraft.yaml
+++ b/charms/ovn-relay-k8s/charmcraft.yaml
@@ -29,4 +29,3 @@ parts:
       - cryptography
       - jsonschema
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/ovn-relay-k8s/fetch-libs.sh b/charms/ovn-relay-k8s/fetch-libs.sh
deleted file mode 100755
index bbf34c7b..00000000
--- a/charms/ovn-relay-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
-charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
diff --git a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644
index 56cca01a..00000000
--- a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py
+++ /dev/null
@@ -1,342 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
-cat << EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Bound with custom events by providing `refresh_event` argument:
-For example, you would like to have a configurable port in your charm and want to apply
-service patch every time charm config is changed.
-
-```python
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(
-        self,
-        [port],
-        refresh_event=self.on.config_changed
-    )
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal, Optional, Union
-
-from lightkube import ApiError, Client
-from lightkube.core import exceptions
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import BoundEvent, Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: Optional[str] = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: Optional[dict] = None,
-        additional_selectors: Optional[dict] = None,
-        additional_annotations: Optional[dict] = None,
-        *,
-        refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-            refresh_event: an optional bound event or list of bound events which
-                will be observed to re-apply the patch (e.g. on port change).
-                The `install` and `upgrade-charm` events would be observed regardless.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-        self.framework.observe(charm.on.update_status, self._patch)
-
-        # apply user defined events
-        if refresh_event:
-            if not isinstance(refresh_event, list):
-                refresh_event = [refresh_event]
-
-            for evt in refresh_event:
-                self.framework.observe(evt, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: Optional[str] = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: Optional[dict] = None,
-        additional_selectors: Optional[dict] = None,
-        additional_annotations: Optional[dict] = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        try:
-            client = Client()
-        except exceptions.ConfigError as e:
-            logger.warning("Error creating k8s client: %s", e)
-            return
-
-        try:
-            if self._is_patched(client):
-                return
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        return self._is_patched(client)
-
-    def _is_patched(self, client: Client) -> bool:
-        # Get the relevant service from the cluster
-        try:
-            service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        except ApiError as e:
-            if e.status.code == 404 and self.service_name != self._app:
-                return False
-            else:
-                logger.error("Kubernetes service get failed: %s", str(e))
-                raise
-
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [
-            (p.port, p.targetPort) for p in service.spec.ports  # type: ignore[attr-defined]
-        ]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
deleted file mode 100644
index 732679a6..00000000
--- a/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""TODO: Add a proper docstring here.
-
-This is a placeholder docstring for this charm library. Docstrings are
-presented on Charmhub and updated whenever you push a new version of the
-library.
-
-Complete documentation about creating and documenting libraries can be found
-in the SDK docs at https://juju.is/docs/sdk/libraries.
-
-See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
-share and consume charm libraries. They serve to enhance collaboration
-between charmers. Use a charmer's libraries for classes that handle
-integration with their charm.
-
-Bear in mind that new revisions of the different major API versions (v0, v1,
-v2 etc) are maintained independently.  You can continue to update v0 and v1
-after you have pushed v3.
-
-Markdown is supported, following the CommonMark specification.
-"""
-
-import logging
-import typing
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "114b7bb1970445daa61650e451f9da62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 3
-
-
-# TODO: add your code here! Happy coding!
-class OVSDBCMSConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSConnectedEvent)
-    ready = EventSource(OVSDBCMSReadyEvent)
-    goneaway = EventSource(OVSDBCMSGoneAwayEvent)
-
-
-class OVSDBCMSRequires(Object):
-    """
-    OVSDBCMSRequires class
-    """
-
-    on = OVSDBCMSServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """OVSDBCMS relation joined."""
-        logging.debug("OVSDBCMSRequires on_joined")
-        self.on.connected.emit()
-
-    def bound_hostnames(self):
-        return self.get_all_unit_values("bound-hostname")
-
-    def bound_addresses(self):
-        return self.get_all_unit_values("bound-address")
-
-    def remote_ready(self):
-        return all(self.bound_hostnames()) or all(self.bound_addresses())
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """OVSDBCMS relation changed."""
-        logging.debug("OVSDBCMSRequires on_changed")
-        if self.remote_ready():
-            self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """OVSDBCMS relation broken."""
-        logging.debug("OVSDBCMSRequires on_broken")
-        self.on.goneaway.emit()
-
-    def get_all_unit_values(self, key: str) -> typing.List[str]:
-        """Retrieve value for key from all related units."""
-        values = []
-        relation = self.framework.model.get_relation(self.relation_name)
-        if relation:
-            for unit in relation.units:
-                values.append(relation.data[unit].get(key))
-        return values
-
-
-
-class OVSDBCMSClientConnectedEvent(EventBase):
-    """OVSDBCMS connected Event."""
-
-    pass
-
-
-class OVSDBCMSClientReadyEvent(EventBase):
-    """OVSDBCMS ready for use Event."""
-
-    pass
-
-
-class OVSDBCMSClientGoneAwayEvent(EventBase):
-    """OVSDBCMS relation has gone-away Event"""
-
-    pass
-
-
-class OVSDBCMSClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(OVSDBCMSClientConnectedEvent)
-    ready = EventSource(OVSDBCMSClientReadyEvent)
-    goneaway = EventSource(OVSDBCMSClientGoneAwayEvent)
-
-
-class OVSDBCMSProvides(Object):
-    """
-    OVSDBCMSProvides class
-    """
-
-    on = OVSDBCMSClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_ovsdb_cms_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_ovsdb_cms_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_ovsdb_cms_relation_broken,
-        )
-
-    def _on_ovsdb_cms_relation_joined(self, event):
-        """Handle ovsdb-cms joined."""
-        logging.debug("OVSDBCMSProvides on_joined")
-        self.on.connected.emit()
-
-    def _on_ovsdb_cms_relation_changed(self, event):
-        """Handle ovsdb-cms changed."""
-        logging.debug("OVSDBCMSProvides on_changed")
-        self.on.ready.emit()
-
-    def _on_ovsdb_cms_relation_broken(self, event):
-        """Handle ovsdb-cms broken."""
-        logging.debug("OVSDBCMSProvides on_departed")
-        self.on.goneaway.emit()
-
-    def set_unit_data(self, settings: typing.Dict[str, str]) -> None:
-        """Publish settings on the peer unit data bag."""
-        relations = self.framework.model.relations[self.relation_name]
-        for relation in relations:
-            for k, v in settings.items():
-                relation.data[self.model.unit][k] = v
diff --git a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
deleted file mode 100644
index 1eda19bf..00000000
--- a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
+++ /dev/null
@@ -1,1261 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Library for the tls-certificates relation.
-
-This library contains the Requires and Provides classes for handling the tls-certificates
-interface.
-
-## Getting Started
-From a charm directory, fetch the library using `charmcraft`:
-
-```shell
-charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
-```
-
-Add the following libraries to the charm's `requirements.txt` file:
-- jsonschema
-- cryptography
-
-Add the following section to the charm's `charmcraft.yaml` file:
-```yaml
-parts:
-  charm:
-    build-packages:
-      - libffi-dev
-      - libssl-dev
-      - rustc
-      - cargo
-```
-
-### Provider charm
-The provider charm is the charm providing certificates to another charm that requires them. In
-this example, the provider charm is storing its private key using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateCreationRequestEvent,
-    CertificateRevocationRequestEvent,
-    TLSCertificatesProvidesV1,
-    generate_private_key,
-)
-from ops.charm import CharmBase, InstallEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-def generate_ca(private_key: bytes, subject: str) -> str:
-    return "whatever ca content"
-
-
-def generate_certificate(ca: str, private_key: str, csr: str) -> str:
-    return "Whatever certificate"
-
-
-class ExampleProviderCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.certificates = TLSCertificatesProvidesV1(self, "certificates")
-        self.framework.observe(
-            self.certificates.on.certificate_request, self._on_certificate_request
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_revoked, self._on_certificate_revocation_request
-        )
-        self.framework.observe(self.on.install, self._on_install)
-
-    def _on_install(self, event: InstallEvent) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        ca_certificate = generate_ca(private_key=private_key, subject="whatever")
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {
-                "private_key_password": "banana",
-                "private_key": private_key,
-                "ca_certificate": ca_certificate,
-            }
-        )
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        certificate = generate_certificate(
-            ca=ca_certificate,
-            private_key=private_key,
-            csr=event.certificate_signing_request,
-        )
-
-        self.certificates.set_relation_certificate(
-            certificate=certificate,
-            certificate_signing_request=event.certificate_signing_request,
-            ca=ca_certificate,
-            chain=[ca_certificate, certificate],
-            relation_id=event.relation_id,
-        )
-
-    def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
-        # Do what you want to do with this information
-        pass
-
-
-if __name__ == "__main__":
-    main(ExampleProviderCharm)
-```
-
-### Requirer charm
-The requirer charm is the charm requiring certificates from another charm that provides them. In
-this example, the requirer charm is storing its certificates using a peer relation interface called
-`replicas`.
-
-Example:
-```python
-from charms.tls_certificates_interface.v1.tls_certificates import (
-    CertificateAvailableEvent,
-    CertificateExpiringEvent,
-    TLSCertificatesRequiresV1,
-    generate_csr,
-    generate_private_key,
-)
-from ops.charm import CharmBase, RelationJoinedEvent
-from ops.main import main
-from ops.model import ActiveStatus, WaitingStatus
-
-
-class ExampleRequirerCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.cert_subject = "whatever"
-        self.certificates = TLSCertificatesRequiresV1(self, "certificates")
-        self.framework.observe(self.on.install, self._on_install)
-        self.framework.observe(
-            self.on.certificates_relation_joined, self._on_certificates_relation_joined
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_available, self._on_certificate_available
-        )
-        self.framework.observe(
-            self.certificates.on.certificate_expiring, self._on_certificate_expiring
-        )
-
-    def _on_install(self, event) -> None:
-        private_key_password = b"banana"
-        private_key = generate_private_key(password=private_key_password)
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update(
-            {"private_key_password": "banana", "private_key": private_key.decode()}
-        )
-
-    def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        replicas_relation.data[self.app].update({"csr": csr.decode()})
-        self.certificates.request_certificate_creation(certificate_signing_request=csr)
-
-    def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        replicas_relation.data[self.app].update({"certificate": event.certificate})
-        replicas_relation.data[self.app].update({"ca": event.ca})
-        replicas_relation.data[self.app].update({"chain": event.chain})
-        self.unit.status = ActiveStatus()
-
-    def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
-        replicas_relation = self.model.get_relation("replicas")
-        if not replicas_relation:
-            self.unit.status = WaitingStatus("Waiting for peer relation to be created")
-            event.defer()
-            return
-        old_csr = replicas_relation.data[self.app].get("csr")
-        private_key_password = replicas_relation.data[self.app].get("private_key_password")
-        private_key = replicas_relation.data[self.app].get("private_key")
-        new_csr = generate_csr(
-            private_key=private_key.encode(),
-            private_key_password=private_key_password.encode(),
-            subject=self.cert_subject,
-        )
-        self.certificates.request_certificate_renewal(
-            old_certificate_signing_request=old_csr,
-            new_certificate_signing_request=new_csr,
-        )
-        replicas_relation.data[self.app].update({"csr": new_csr.decode()})
-
-
-if __name__ == "__main__":
-    main(ExampleRequirerCharm)
-```
-"""  # noqa: D405, D410, D411, D214, D416
-
-import copy
-import json
-import logging
-import uuid
-from datetime import datetime, timedelta
-from ipaddress import IPv4Address
-from typing import Dict, List, Optional
-
-from cryptography import x509
-from cryptography.hazmat._oid import ExtensionOID
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.x509.extensions import Extension, ExtensionNotFound
-from jsonschema import exceptions, validate  # type: ignore[import]
-from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent
-from ops.framework import EventBase, EventSource, Handle, Object
-
-# The unique Charmhub library identifier, never change it
-LIBID = "afd8c2bccf834997afce12c2706d2ede"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 10
-
-REQUIRER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` requirer root schema",
-    "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.",  # noqa: E501
-    "examples": [
-        {
-            "certificate_signing_requests": [
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-                {
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n"  # noqa: E501
-                },
-            ]
-        }
-    ],
-    "properties": {
-        "certificate_signing_requests": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {"certificate_signing_request": {"type": "string"}},
-                "required": ["certificate_signing_request"],
-            },
-        }
-    },
-    "required": ["certificate_signing_requests"],
-    "additionalProperties": True,
-}
-
-PROVIDER_JSON_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json",  # noqa: E501
-    "type": "object",
-    "title": "`tls_certificates` provider root schema",
-    "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.",  # noqa: E501
-    "example": [
-        {
-            "certificates": [
-                {
-                    "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n",  # noqa: E501
-                    "chain": [
-                        "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n"  # noqa: E501, W505
-                    ],
-                    "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n",  # noqa: E501
-                    "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",  # noqa: E501
-                }
-            ]
-        }
-    ],
-    "properties": {
-        "certificates": {
-            "$id": "#/properties/certificates",
-            "type": "array",
-            "items": {
-                "$id": "#/properties/certificates/items",
-                "type": "object",
-                "required": ["certificate_signing_request", "certificate", "ca", "chain"],
-                "properties": {
-                    "certificate_signing_request": {
-                        "$id": "#/properties/certificates/items/certificate_signing_request",
-                        "type": "string",
-                    },
-                    "certificate": {
-                        "$id": "#/properties/certificates/items/certificate",
-                        "type": "string",
-                    },
-                    "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
-                    "chain": {
-                        "$id": "#/properties/certificates/items/chain",
-                        "type": "array",
-                        "items": {
-                            "type": "string",
-                            "$id": "#/properties/certificates/items/chain/items",
-                        },
-                    },
-                },
-                "additionalProperties": True,
-            },
-        }
-    },
-    "required": ["certificates"],
-    "additionalProperties": True,
-}
-
-
-logger = logging.getLogger(__name__)
-
-
-class CertificateAvailableEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is available."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-class CertificateExpiringEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is almost expired."""
-
-    def __init__(self, handle, certificate: str, expiry: str):
-        """CertificateExpiringEvent.
-
-        Args:
-            handle (Handle): Juju framework handle
-            certificate (str): TLS Certificate
-            expiry (str): Datetime string reprensenting the time at which the certificate
-                won't be valid anymore.
-        """
-        super().__init__(handle)
-        self.certificate = certificate
-        self.expiry = expiry
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate, "expiry": self.expiry}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.expiry = snapshot["expiry"]
-
-
-class CertificateExpiredEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is expired."""
-
-    def __init__(self, handle: Handle, certificate: str):
-        super().__init__(handle)
-        self.certificate = certificate
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {"certificate": self.certificate}
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-
-
-class CertificateCreationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate is required."""
-
-    def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int):
-        super().__init__(handle)
-        self.certificate_signing_request = certificate_signing_request
-        self.relation_id = relation_id
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate_signing_request": self.certificate_signing_request,
-            "relation_id": self.relation_id,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.relation_id = snapshot["relation_id"]
-
-
-class CertificateRevocationRequestEvent(EventBase):
-    """Charm Event triggered when a TLS certificate needs to be revoked."""
-
-    def __init__(
-        self,
-        handle: Handle,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: str,
-    ):
-        super().__init__(handle)
-        self.certificate = certificate
-        self.certificate_signing_request = certificate_signing_request
-        self.ca = ca
-        self.chain = chain
-
-    def snapshot(self) -> dict:
-        """Returns snapshot."""
-        return {
-            "certificate": self.certificate,
-            "certificate_signing_request": self.certificate_signing_request,
-            "ca": self.ca,
-            "chain": self.chain,
-        }
-
-    def restore(self, snapshot: dict):
-        """Restores snapshot."""
-        self.certificate = snapshot["certificate"]
-        self.certificate_signing_request = snapshot["certificate_signing_request"]
-        self.ca = snapshot["ca"]
-        self.chain = snapshot["chain"]
-
-
-def _load_relation_data(raw_relation_data: dict) -> dict:
-    """Loads relation data from the relation data bag.
-
-    Json loads all data.
-
-    Args:
-        raw_relation_data: Relation data from the databag
-
-    Returns:
-        dict: Relation data in dict format.
-    """
-    certificate_data = dict()
-    for key in raw_relation_data:
-        try:
-            certificate_data[key] = json.loads(raw_relation_data[key])
-        except (json.decoder.JSONDecodeError, TypeError):
-            certificate_data[key] = raw_relation_data[key]
-    return certificate_data
-
-
-def generate_ca(
-    private_key: bytes,
-    subject: str,
-    private_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    country: str = "US",
-) -> bytes:
-    """Generates a CA Certificate.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): Certificate subject
-        private_key_password (bytes): Private key password
-        validity (int): Certificate validity time (in days)
-        country (str): Certificate Issuing country
-
-    Returns:
-        bytes: CA Certificate.
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    subject = issuer = x509.Name(
-        [
-            x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
-            x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
-        ]
-    )
-    subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
-        private_key_object.public_key()  # type: ignore[arg-type]
-    )
-    subject_identifier = key_identifier = subject_identifier_object.public_bytes()
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(private_key_object.public_key())  # type: ignore[arg-type]
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-        .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
-        .add_extension(
-            x509.AuthorityKeyIdentifier(
-                key_identifier=key_identifier,
-                authority_cert_issuer=None,
-                authority_cert_serial_number=None,
-            ),
-            critical=False,
-        )
-        .add_extension(
-            x509.BasicConstraints(ca=True, path_length=None),
-            critical=True,
-        )
-        .sign(private_key_object, hashes.SHA256())  # type: ignore[arg-type]
-    )
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_certificate(
-    csr: bytes,
-    ca: bytes,
-    ca_key: bytes,
-    ca_key_password: Optional[bytes] = None,
-    validity: int = 365,
-    alt_names: List[str] = None,
-) -> bytes:
-    """Generates a TLS certificate based on a CSR.
-
-    Args:
-        csr (bytes): CSR
-        ca (bytes): CA Certificate
-        ca_key (bytes): CA private key
-        ca_key_password: CA private key password
-        validity (int): Certificate validity (in days)
-        alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
-
-    Returns:
-        bytes: Certificate
-    """
-    csr_object = x509.load_pem_x509_csr(csr)
-    subject = csr_object.subject
-    issuer = x509.load_pem_x509_certificate(ca).issuer
-    private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
-
-    certificate_builder = (
-        x509.CertificateBuilder()
-        .subject_name(subject)
-        .issuer_name(issuer)
-        .public_key(csr_object.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.utcnow())
-        .not_valid_after(datetime.utcnow() + timedelta(days=validity))
-    )
-
-    extensions_list = csr_object.extensions
-    san_ext: Optional[x509.Extension] = None
-    if alt_names:
-        full_sans_dns = alt_names.copy()
-        try:
-            loaded_san_ext = csr_object.extensions.get_extension_for_class(
-                x509.SubjectAlternativeName
-            )
-            full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName))
-        except ExtensionNotFound:
-            pass
-        finally:
-            san_ext = Extension(
-                ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
-                False,
-                x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]),
-            )
-            if not extensions_list:
-                extensions_list = x509.Extensions([san_ext])
-
-    for extension in extensions_list:
-        if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext:
-            extension = san_ext
-
-        certificate_builder = certificate_builder.add_extension(
-            extension.value,
-            critical=extension.critical,
-        )
-    certificate_builder._version = x509.Version.v3
-    cert = certificate_builder.sign(private_key, hashes.SHA256())  # type: ignore[arg-type]
-    return cert.public_bytes(serialization.Encoding.PEM)
-
-
-def generate_pfx_package(
-    certificate: bytes,
-    private_key: bytes,
-    package_password: str,
-    private_key_password: Optional[bytes] = None,
-) -> bytes:
-    """Generates a PFX package to contain the TLS certificate and private key.
-
-    Args:
-        certificate (bytes): TLS certificate
-        private_key (bytes): Private key
-        package_password (str): Password to open the PFX package
-        private_key_password (bytes): Private key password
-
-    Returns:
-        bytes:
-    """
-    private_key_object = serialization.load_pem_private_key(
-        private_key, password=private_key_password
-    )
-    certificate_object = x509.load_pem_x509_certificate(certificate)
-    name = certificate_object.subject.rfc4514_string()
-    pfx_bytes = pkcs12.serialize_key_and_certificates(
-        name=name.encode(),
-        cert=certificate_object,
-        key=private_key_object,  # type: ignore[arg-type]
-        cas=None,
-        encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()),
-    )
-    return pfx_bytes
-
-
-def generate_private_key(
-    password: Optional[bytes] = None,
-    key_size: int = 2048,
-    public_exponent: int = 65537,
-) -> bytes:
-    """Generates a private key.
-
-    Args:
-        password (bytes): Password for decrypting the private key
-        key_size (int): Key size in bytes
-        public_exponent: Public exponent.
-
-    Returns:
-        bytes: Private Key
-    """
-    private_key = rsa.generate_private_key(
-        public_exponent=public_exponent,
-        key_size=key_size,
-    )
-    key_bytes = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.TraditionalOpenSSL,
-        encryption_algorithm=serialization.BestAvailableEncryption(password)
-        if password
-        else serialization.NoEncryption(),
-    )
-    return key_bytes
-
-
-def generate_csr(
-    private_key: bytes,
-    subject: str,
-    add_unique_id_to_subject_name: bool = True,
-    organization: str = None,
-    email_address: str = None,
-    country_name: str = None,
-    private_key_password: Optional[bytes] = None,
-    sans: Optional[List[str]] = None,
-    sans_oid: Optional[List[str]] = None,
-    sans_ip: Optional[List[str]] = None,
-    sans_dns: Optional[List[str]] = None,
-    additional_critical_extensions: Optional[List] = None,
-) -> bytes:
-    """Generates a CSR using private key and subject.
-
-    Args:
-        private_key (bytes): Private key
-        subject (str): CSR Subject.
-        add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
-            subject name. Always leave to "True" when the CSR is used to request certificates
-            using the tls-certificates relation.
-        organization (str): Name of organization.
-        email_address (str): Email address.
-        country_name (str): Country Name.
-        private_key_password (bytes): Private key password
-        sans (list): Use sans_dns - this will be deprecated in a future release
-            List of DNS subject alternative names (keeping it for now for backward compatibility)
-        sans_oid (list): List of registered ID SANs
-        sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
-        sans_ip (list): List of IP subject alternative names
-        additional_critical_extensions (list): List if critical additional extension objects.
-            Object must be a x509 ExtensionType.
-
-    Returns:
-        bytes: CSR
-    """
-    signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
-    subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
-    if add_unique_id_to_subject_name:
-        unique_identifier = uuid.uuid4()
-        subject_name.append(
-            x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
-        )
-    if organization:
-        subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
-    if email_address:
-        subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
-    if country_name:
-        subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
-    csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
-
-    _sans: List[x509.GeneralName] = []
-    if sans_oid:
-        _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
-    if sans_ip:
-        _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
-    if sans:
-        _sans.extend([x509.DNSName(san) for san in sans])
-    if sans_dns:
-        _sans.extend([x509.DNSName(san) for san in sans_dns])
-    if _sans:
-        csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
-
-    if additional_critical_extensions:
-        for extension in additional_critical_extensions:
-            csr = csr.add_extension(extension, critical=True)
-
-    signed_certificate = csr.sign(signing_key, hashes.SHA256())  # type: ignore[arg-type]
-    return signed_certificate.public_bytes(serialization.Encoding.PEM)
-
-
-class CertificatesProviderCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates provider charm can leverage."""
-
-    certificate_creation_request = EventSource(CertificateCreationRequestEvent)
-    certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
-
-
-class CertificatesRequirerCharmEvents(CharmEvents):
-    """List of events that the TLS Certificates requirer charm can leverage."""
-
-    certificate_available = EventSource(CertificateAvailableEvent)
-    certificate_expiring = EventSource(CertificateExpiringEvent)
-    certificate_expired = EventSource(CertificateExpiredEvent)
-
-
-class TLSCertificatesProvidesV1(Object):
-    """TLS certificates provider class to be instantiated by TLS certificates providers."""
-
-    on = CertificatesProviderCharmEvents()
-
-    def __init__(self, charm: CharmBase, relationship_name: str):
-        super().__init__(charm, relationship_name)
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.charm = charm
-        self.relationship_name = relationship_name
-
-    def _add_certificate(
-        self,
-        relation_id: int,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-    ) -> None:
-        """Adds certificate to relation data.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate Signing Request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_certificate = {
-            "certificate": certificate,
-            "certificate_signing_request": certificate_signing_request,
-            "ca": ca,
-            "chain": chain,
-        }
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        if new_certificate in certificates:
-            logger.info("Certificate already in relation data - Doing nothing")
-            return
-        certificates.append(new_certificate)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    def _remove_certificate(
-        self,
-        relation_id: int,
-        certificate: str = None,
-        certificate_signing_request: str = None,
-    ) -> None:
-        """Removes certificate from a given relation based on user provided certificate or csr.
-
-        Args:
-            relation_id (int): Relation id
-            certificate (str): Certificate (optional)
-            certificate_signing_request: Certificate signing request (optional)
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(
-            relation_name=self.relationship_name,
-            relation_id=relation_id,
-        )
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
-            )
-        provider_relation_data = _load_relation_data(relation.data[self.charm.app])
-        provider_certificates = provider_relation_data.get("certificates", [])
-        certificates = copy.deepcopy(provider_certificates)
-        for certificate_dict in certificates:
-            if certificate and certificate_dict["certificate"] == certificate:
-                certificates.remove(certificate_dict)
-            if (
-                certificate_signing_request
-                and certificate_dict["certificate_signing_request"] == certificate_signing_request
-            ):
-                certificates.remove(certificate_dict)
-        relation.data[self.model.app]["certificates"] = json.dumps(certificates)
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Uses JSON schema validator to validate relation data content.
-
-        Args:
-            certificates_data (dict): Certificate data dictionary as retrieved from relation data.
-
-        Returns:
-            bool: True/False depending on whether the relation data follows the json schema.
-        """
-        try:
-            validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def revoke_all_certificates(self) -> None:
-        """Revokes all certificates of this provider.
-
-        This method is meant to be used when the Root CA has changed.
-        """
-        for relation in self.model.relations[self.relationship_name]:
-            relation.data[self.model.app]["certificates"] = json.dumps([])
-
-    def set_relation_certificate(
-        self,
-        certificate: str,
-        certificate_signing_request: str,
-        ca: str,
-        chain: List[str],
-        relation_id: int,
-    ) -> None:
-        """Adds certificates to relation data.
-
-        Args:
-            certificate (str): Certificate
-            certificate_signing_request (str): Certificate signing request
-            ca (str): CA Certificate
-            chain (list): CA Chain
-            relation_id (int): Juju relation ID
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        self._remove_certificate(
-            certificate_signing_request=certificate_signing_request.strip(),
-            relation_id=relation_id,
-        )
-        self._add_certificate(
-            relation_id=relation_id,
-            certificate=certificate.strip(),
-            certificate_signing_request=certificate_signing_request.strip(),
-            ca=ca.strip(),
-            chain=[cert.strip() for cert in chain],
-        )
-
-    def remove_certificate(self, certificate: str) -> None:
-        """Removes a given certificate from relation data.
-
-        Args:
-            certificate (str): TLS Certificate
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.relations[self.relationship_name]
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        for certificate_relation in certificates_relation:
-            self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed event.
-
-        Looks at the relation data and either emits:
-        - certificate request event: If the unit relation data contains a CSR for which
-            a certificate does not exist in the provider relation data.
-        - certificate revocation event: If the provider relation data contains a CSR for which
-            a csr does not exist in the requirer relation data.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        assert event.unit is not None
-        requirer_relation_data = _load_relation_data(event.relation.data[event.unit])
-        provider_relation_data = _load_relation_data(event.relation.data[self.charm.app])
-        if not self._relation_data_is_valid(requirer_relation_data):
-            logger.warning(
-                f"Relation data did not pass JSON Schema validation: {requirer_relation_data}"
-            )
-            return
-        provider_certificates = provider_relation_data.get("certificates", [])
-        requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-        provider_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in provider_certificates
-        ]
-        requirer_unit_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in requirer_csrs
-        ]
-        for certificate_signing_request in requirer_unit_csrs:
-            if certificate_signing_request not in provider_csrs:
-                self.on.certificate_creation_request.emit(
-                    certificate_signing_request=certificate_signing_request,
-                    relation_id=event.relation.id,
-                )
-        self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
-
-    def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
-        """Revokes certificates for which no unit has a CSR.
-
-        Goes through all generated certificates and compare agains the list of CSRS for all units
-        of a given relationship.
-
-        Args:
-            relation_id (int): Relation id
-
-        Returns:
-            None
-        """
-        certificates_relation = self.model.get_relation(
-            relation_name=self.relationship_name, relation_id=relation_id
-        )
-        if not certificates_relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app])
-        list_of_csrs: List[str] = []
-        for unit in certificates_relation.units:
-            requirer_relation_data = _load_relation_data(certificates_relation.data[unit])
-            requirer_csrs = requirer_relation_data.get("certificate_signing_requests", [])
-            list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs)
-        provider_certificates = provider_relation_data.get("certificates", [])
-        for certificate in provider_certificates:
-            if certificate["certificate_signing_request"] not in list_of_csrs:
-                self.on.certificate_revocation_request.emit(
-                    certificate=certificate["certificate"],
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-                self.remove_certificate(certificate=certificate["certificate"])
-
-
-class TLSCertificatesRequiresV1(Object):
-    """TLS certificates requirer class to be instantiated by TLS certificates requirers."""
-
-    on = CertificatesRequirerCharmEvents()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relationship_name: str,
-        expiry_notification_time: int = 168,
-    ):
-        """Generates/use private key and observes relation changed event.
-
-        Args:
-            charm: Charm object
-            relationship_name: Juju relation name
-            expiry_notification_time (int): Time difference between now and expiry (in hours).
-                Used to trigger the CertificateExpiring event. Default: 7 days.
-        """
-        super().__init__(charm, relationship_name)
-        self.relationship_name = relationship_name
-        self.charm = charm
-        self.expiry_notification_time = expiry_notification_time
-        self.framework.observe(
-            charm.on[relationship_name].relation_changed, self._on_relation_changed
-        )
-        self.framework.observe(charm.on.update_status, self._on_update_status)
-
-    @property
-    def _requirer_csrs(self) -> List[Dict[str, str]]:
-        """Returns list of requirer CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
-        return requirer_relation_data.get("certificate_signing_requests", [])
-
-    @property
-    def _provider_certificates(self) -> List[Dict[str, str]]:
-        """Returns list of provider CSR's from relation data."""
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(f"Relation {self.relationship_name} does not exist")
-        if not relation.app:
-            raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist")
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        return provider_relation_data.get("certificates", [])
-
-    def _add_requirer_csr(self, csr: str) -> None:
-        """Adds CSR to relation data.
-
-        Args:
-            csr (str): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        new_csr_dict = {"certificate_signing_request": csr}
-        if new_csr_dict in self._requirer_csrs:
-            logger.info("CSR already in relation data - Doing nothing")
-            return
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        requirer_csrs.append(new_csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def _remove_requirer_csr(self, csr: str) -> None:
-        """Removes CSR from relation data.
-
-        Args:
-            csr (str): Certificate signing request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            raise RuntimeError(
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-        requirer_csrs = copy.deepcopy(self._requirer_csrs)
-        csr_dict = {"certificate_signing_request": csr}
-        if csr_dict not in requirer_csrs:
-            logger.info("CSR not in relation data - Doing nothing")
-            return
-        requirer_csrs.remove(csr_dict)
-        relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs)
-
-    def request_certificate_creation(self, certificate_signing_request: bytes) -> None:
-        """Request TLS certificate to provider charm.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            message = (
-                f"Relation {self.relationship_name} does not exist - "
-                f"The certificate request can't be completed"
-            )
-            logger.error(message)
-            raise RuntimeError(message)
-        self._add_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate request sent to provider")
-
-    def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
-        """Removes CSR from relation data.
-
-        The provider of this relation is then expected to remove certificates associated to this
-        CSR from the relation data as well and emit a request_certificate_revocation event for the
-        provider charm to interpret.
-
-        Args:
-            certificate_signing_request (bytes): Certificate Signing Request
-
-        Returns:
-            None
-        """
-        self._remove_requirer_csr(certificate_signing_request.decode().strip())
-        logger.info("Certificate revocation sent to provider")
-
-    def request_certificate_renewal(
-        self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
-    ) -> None:
-        """Renews certificate.
-
-        Removes old CSR from relation data and adds new one.
-
-        Args:
-            old_certificate_signing_request: Old CSR
-            new_certificate_signing_request: New CSR
-
-        Returns:
-            None
-        """
-        try:
-            self.request_certificate_revocation(
-                certificate_signing_request=old_certificate_signing_request
-            )
-        except RuntimeError:
-            logger.warning("Certificate revocation failed.")
-        self.request_certificate_creation(
-            certificate_signing_request=new_certificate_signing_request
-        )
-        logger.info("Certificate renewal request completed.")
-
-    @staticmethod
-    def _relation_data_is_valid(certificates_data: dict) -> bool:
-        """Checks whether relation data is valid based on json schema.
-
-        Args:
-            certificates_data: Certificate data in dict format.
-
-        Returns:
-            bool: Whether relation data is valid.
-        """
-        try:
-            validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA)
-            return True
-        except exceptions.ValidationError:
-            return False
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Handler triggerred on relation changed events.
-
-        Args:
-            event: Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{event.relation.data[relation.app]}"
-            )
-            return
-        requirer_csrs = [
-            certificate_creation_request["certificate_signing_request"]
-            for certificate_creation_request in self._requirer_csrs
-        ]
-        for certificate in self._provider_certificates:
-            if certificate["certificate_signing_request"] in requirer_csrs:
-                self.on.certificate_available.emit(
-                    certificate_signing_request=certificate["certificate_signing_request"],
-                    certificate=certificate["certificate"],
-                    ca=certificate["ca"],
-                    chain=certificate["chain"],
-                )
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Triggered on update status event.
-
-        Goes through each certificate in the "certificates" relation and checks their expiry date.
-        If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if
-        they are expired, emits a CertificateExpiredEvent.
-
-        Args:
-            event (UpdateStatusEvent): Juju event
-
-        Returns:
-            None
-        """
-        relation = self.model.get_relation(self.relationship_name)
-        if not relation:
-            logger.warning(f"No relation: {self.relationship_name}")
-            return
-        if not relation.app:
-            logger.warning(f"No remote app in relation: {self.relationship_name}")
-            return
-        provider_relation_data = _load_relation_data(relation.data[relation.app])
-        if not self._relation_data_is_valid(provider_relation_data):
-            logger.warning(
-                f"Provider relation data did not pass JSON Schema validation: "
-                f"{relation.data[relation.app]}"
-            )
-            return
-        for certificate_dict in self._provider_certificates:
-            certificate = certificate_dict["certificate"]
-            try:
-                certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
-            except ValueError:
-                logger.warning("Could not load certificate.")
-                continue
-            time_difference = certificate_object.not_valid_after - datetime.utcnow()
-            if time_difference.total_seconds() < 0:
-                logger.warning("Certificate is expired")
-                self.on.certificate_expired.emit(certificate=certificate)
-                self.request_certificate_revocation(certificate.encode())
-                continue
-            if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60):
-                logger.warning("Certificate almost expired")
-                self.on.certificate_expiring.emit(
-                    certificate=certificate, expiry=certificate_object.not_valid_after.isoformat()
-                )
diff --git a/charms/ovn-relay-k8s/osci.yaml b/charms/ovn-relay-k8s/osci.yaml
deleted file mode 100644
index 352efb71..00000000
--- a/charms/ovn-relay-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: ovn-relay-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 23.09/edge
diff --git a/charms/ovn-relay-k8s/pyproject.toml b/charms/ovn-relay-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/ovn-relay-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/ovn-relay-k8s/rename.sh b/charms/ovn-relay-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/ovn-relay-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/ovn-relay-k8s/requirements.txt b/charms/ovn-relay-k8s/requirements.txt
index 9211d667..ddbedec1 100644
--- a/charms/ovn-relay-k8s/requirements.txt
+++ b/charms/ovn-relay-k8s/requirements.txt
@@ -11,4 +11,5 @@ lightkube
 lightkube-models
 ops
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+# From ops_sunbeam
+tenacity
diff --git a/charms/ovn-relay-k8s/test-requirements.txt b/charms/ovn-relay-k8s/test-requirements.txt
deleted file mode 100644
index da3b04b7..00000000
--- a/charms/ovn-relay-k8s/test-requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-stestr
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py
index f4654d66..475e4d55 100644
--- a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py
+++ b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py
@@ -16,9 +16,8 @@
 
 """Tests for OVN relay."""
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _OVNRelayOperatorCharm(charm.OVNRelayOperatorCharm):
diff --git a/charms/ovn-relay-k8s/tox.ini b/charms/ovn-relay-k8s/tox.ini
deleted file mode 100644
index b0d1dd7b..00000000
--- a/charms/ovn-relay-k8s/tox.ini
+++ /dev/null
@@ -1,169 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-  HOME
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-deps =
-    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-    git+https://opendev.org/openstack/tempest.git#egg=tempest
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-deps = {[testenv:func-noop]deps}
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-
-[flake8]
-ignore=E226,W504
diff --git a/charms/placement-k8s/.flake8 b/charms/placement-k8s/.flake8
deleted file mode 100644
index 8ef84fcd..00000000
--- a/charms/placement-k8s/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 99
-select: E,W,F,C,N
-exclude:
-  venv
-  .git
-  build
-  dist
-  *.egg_info
diff --git a/charms/placement-k8s/.gitignore b/charms/placement-k8s/.gitignore
deleted file mode 100644
index 73f116c9..00000000
--- a/charms/placement-k8s/.gitignore
+++ /dev/null
@@ -1,11 +0,0 @@
-venv/
-build/
-*.charm
-*.swp
-
-.coverage
-__pycache__/
-*.py[cod]
-.tox
-.stestr/
-tempest.log
diff --git a/charms/placement-k8s/.gitreview b/charms/placement-k8s/.gitreview
deleted file mode 100644
index 5d944736..00000000
--- a/charms/placement-k8s/.gitreview
+++ /dev/null
@@ -1,5 +0,0 @@
-[gerrit]
-host=review.opendev.org
-port=29418
-project=openstack/charm-placement-k8s.git
-defaultbranch=main
diff --git a/charms/placement-k8s/.jujuignore b/charms/placement-k8s/.jujuignore
deleted file mode 100644
index 6ccd559e..00000000
--- a/charms/placement-k8s/.jujuignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/venv
-*.py[cod]
-*.charm
diff --git a/charms/placement-k8s/.stestr.conf b/charms/placement-k8s/.stestr.conf
deleted file mode 100644
index e4750de4..00000000
--- a/charms/placement-k8s/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=./tests/unit
-top_dir=./tests
diff --git a/charms/placement-k8s/.zuul.yaml b/charms/placement-k8s/.zuul.yaml
deleted file mode 100644
index 5f56197e..00000000
--- a/charms/placement-k8s/.zuul.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-- project:
-    templates:
-      - openstack-python3-charm-jobs
-      - openstack-cover-jobs
-      - microk8s-func-test
-    vars:
-      charm_build_name: placement-k8s
-      juju_channel: 3.1/stable
-      juju_classic_mode: false
-      microk8s_channel: 1.28-strict/stable
-      microk8s_classic_mode: false
diff --git a/charms/placement-k8s/charmcraft.yaml b/charms/placement-k8s/charmcraft.yaml
index 2fdc318d..9556149e 100644
--- a/charms/placement-k8s/charmcraft.yaml
+++ b/charms/placement-k8s/charmcraft.yaml
@@ -28,4 +28,3 @@ parts:
       - jsonschema
       - pydantic<2.0
       - jinja2
-      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/placement-k8s/fetch-libs.sh b/charms/placement-k8s/fetch-libs.sh
deleted file mode 100755
index e7772471..00000000
--- a/charms/placement-k8s/fetch-libs.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-echo "INFO: Fetching libs from charmhub."
-charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
-charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
-charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
-charmcraft fetch-lib charms.traefik_k8s.v2.ingress
diff --git a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py
deleted file mode 100644
index 53d61912..00000000
--- a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py
+++ /dev/null
@@ -1,496 +0,0 @@
-# Copyright 2022 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.
-
-"""Relation 'requires' side abstraction for database relation.
-
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-— database_created: event emitted when the requested database is created.
-— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
-— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.database_requires import DatabaseRequires
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-"""
-
-import json
-import logging
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0241e088ffa9440fb4e3126349b2fb62"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version.
-LIBPATCH = 4
-
-logger = logging.getLogger(__name__)
-
-
-class DatabaseEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch and Kafka only.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(DatabaseEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-— added — keys that were added.
-— changed — keys that still exist but have new values.
-— deleted — keys that were deleted.
-"""
-
-
-class DatabaseRequires(Object):
-    """Requires-side of the database relation."""
-
-    on = DatabaseEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.database = database_name
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.relations_aliases = relations_aliases
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        # Retrieve the old data from the data key in the local unit relation databag.
-        old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
-        # Retrieve the new data from the event relation databag.
-        new_data = {
-            key: value for key, value in event.relation.data[event.app].items() if key != "data"
-        }
-
-        # These are the keys that were added to the databag and triggered this event.
-        added = new_data.keys() - old_data.keys()
-        # These are the keys that were removed from the databag and triggered this event.
-        deleted = old_data.keys() - new_data.keys()
-        # These are the keys that already existed in the databag,
-        # but had their values changed.
-        changed = {
-            key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
-        }
-
-        # TODO: evaluate the possibility of losing the diff if some error
-        # happens in the charm before the diff is completely checked (DPE-412).
-        # Convert the new_data to a serializable format and save it for a next diff check.
-        event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
-
-        # Return the diff with all possible changes.
-        return Diff(added, changed, deleted)
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
diff --git a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py
deleted file mode 100644
index 62dd9a3f..00000000
--- a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py
+++ /dev/null
@@ -1,525 +0,0 @@
-"""IdentityServiceProvides and Requires module.
-
-
-This library contains the Requires and Provides classes for handling
-the identity_service interface.
-
-Import `IdentityServiceRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "identity_service"
-
-Also provide additional parameters to the charm object:
-    - service
-    - internal_url
-    - public_url
-    - admin_url
-    - region
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
-
-class IdentityServiceClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # IdentityService Requires
-        self.identity_service = IdentityServiceRequires(
-            self, "identity_service",
-            service = "my-service"
-            internal_url = "http://internal-url"
-            public_url = "http://public-url"
-            admin_url = "http://admin-url"
-            region = "region"
-        )
-        self.framework.observe(
-            self.identity_service.on.connected, self._on_identity_service_connected)
-        self.framework.observe(
-            self.identity_service.on.ready, self._on_identity_service_ready)
-        self.framework.observe(
-            self.identity_service.on.goneaway, self._on_identity_service_goneaway)
-
-    def _on_identity_service_connected(self, event):
-        '''React to the IdentityService connected event.
-
-        This event happens when n IdentityService relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_identity_service_ready(self, event):
-        '''React to the IdentityService ready event.
-
-        The IdentityService interface will use the provided config for the
-        request to the identity server.
-        '''
-        # IdentityService Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_identity_service_goneaway(self, event):
-        '''React to the IdentityService goneaway event.
-
-        This event happens when an IdentityService relation is removed.
-        '''
-        # IdentityService Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-import json
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-from ops.model import (
-    Relation,
-    SecretNotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 1
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-
-logger = logging.getLogger(__name__)
-
-
-class IdentityServiceConnectedEvent(EventBase):
-    """IdentityService connected Event."""
-
-    pass
-
-
-class IdentityServiceReadyEvent(EventBase):
-    """IdentityService ready for use Event."""
-
-    pass
-
-
-class IdentityServiceGoneAwayEvent(EventBase):
-    """IdentityService relation has gone-away Event"""
-
-    pass
-
-
-class IdentityServiceServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(IdentityServiceConnectedEvent)
-    ready = EventSource(IdentityServiceReadyEvent)
-    goneaway = EventSource(IdentityServiceGoneAwayEvent)
-
-
-class IdentityServiceRequires(Object):
-    """
-    IdentityServiceRequires class
-    """
-
-    on = IdentityServiceServerEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name: str, service_endpoints: dict,
-                 region: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """IdentityService relation joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.connected.emit()
-        self.register_services(
-            self.service_endpoints,
-            self.region)
-
-    def _on_identity_service_relation_changed(self, event):
-        """IdentityService relation changed."""
-        logging.debug("IdentityService on_changed")
-        try:
-            self.service_password
-            self.on.ready.emit()
-        except (AttributeError, KeyError):
-            pass
-
-    def _on_identity_service_relation_broken(self, event):
-        """IdentityService relation broken."""
-        logging.debug("IdentityService on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _identity_service_rel(self) -> Relation:
-        """The IdentityService relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    def get_remote_app_data(self, key: str) -> str:
-        """Return the value for the given key from remote app data."""
-        data = self._identity_service_rel.data[self._identity_service_rel.app]
-        return data.get(key)
-
-    @property
-    def api_version(self) -> str:
-        """Return the api_version."""
-        return self.get_remote_app_data('api-version')
-
-    @property
-    def auth_host(self) -> str:
-        """Return the auth_host."""
-        return self.get_remote_app_data('auth-host')
-
-    @property
-    def auth_port(self) -> str:
-        """Return the auth_port."""
-        return self.get_remote_app_data('auth-port')
-
-    @property
-    def auth_protocol(self) -> str:
-        """Return the auth_protocol."""
-        return self.get_remote_app_data('auth-protocol')
-
-    @property
-    def internal_host(self) -> str:
-        """Return the internal_host."""
-        return self.get_remote_app_data('internal-host')
-
-    @property
-    def internal_port(self) -> str:
-        """Return the internal_port."""
-        return self.get_remote_app_data('internal-port')
-
-    @property
-    def internal_protocol(self) -> str:
-        """Return the internal_protocol."""
-        return self.get_remote_app_data('internal-protocol')
-
-    @property
-    def admin_domain_name(self) -> str:
-        """Return the admin_domain_name."""
-        return self.get_remote_app_data('admin-domain-name')
-
-    @property
-    def admin_domain_id(self) -> str:
-        """Return the admin_domain_id."""
-        return self.get_remote_app_data('admin-domain-id')
-
-    @property
-    def admin_project_name(self) -> str:
-        """Return the admin_project_name."""
-        return self.get_remote_app_data('admin-project-name')
-
-    @property
-    def admin_project_id(self) -> str:
-        """Return the admin_project_id."""
-        return self.get_remote_app_data('admin-project-id')
-
-    @property
-    def admin_user_name(self) -> str:
-        """Return the admin_user_name."""
-        return self.get_remote_app_data('admin-user-name')
-
-    @property
-    def admin_user_id(self) -> str:
-        """Return the admin_user_id."""
-        return self.get_remote_app_data('admin-user-id')
-
-    @property
-    def service_domain_name(self) -> str:
-        """Return the service_domain_name."""
-        return self.get_remote_app_data('service-domain-name')
-
-    @property
-    def service_domain_id(self) -> str:
-        """Return the service_domain_id."""
-        return self.get_remote_app_data('service-domain-id')
-
-    @property
-    def service_host(self) -> str:
-        """Return the service_host."""
-        return self.get_remote_app_data('service-host')
-
-    @property
-    def service_credentials(self) -> str:
-        """Return the service_credentials secret."""
-        return self.get_remote_app_data('service-credentials')
-
-    @property
-    def service_password(self) -> str:
-        """Return the service_password."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("password")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_port(self) -> str:
-        """Return the service_port."""
-        return self.get_remote_app_data('service-port')
-
-    @property
-    def service_protocol(self) -> str:
-        """Return the service_protocol."""
-        return self.get_remote_app_data('service-protocol')
-
-    @property
-    def service_project_name(self) -> str:
-        """Return the service_project_name."""
-        return self.get_remote_app_data('service-project-name')
-
-    @property
-    def service_project_id(self) -> str:
-        """Return the service_project_id."""
-        return self.get_remote_app_data('service-project-id')
-
-    @property
-    def service_user_name(self) -> str:
-        """Return the service_user_name."""
-        credentials_id = self.get_remote_app_data('service-credentials')
-        if not credentials_id:
-            return None
-
-        try:
-            credentials = self.charm.model.get_secret(id=credentials_id)
-            return credentials.get_content().get("username")
-        except SecretNotFoundError:
-            logger.warning(f"Secret {credentials_id} not found")
-            return None
-
-    @property
-    def service_user_id(self) -> str:
-        """Return the service_user_id."""
-        return self.get_remote_app_data('service-user-id')
-
-    @property
-    def internal_auth_url(self) -> str:
-        """Return the internal_auth_url."""
-        return self.get_remote_app_data('internal-auth-url')
-
-    @property
-    def admin_auth_url(self) -> str:
-        """Return the admin_auth_url."""
-        return self.get_remote_app_data('admin-auth-url')
-
-    @property
-    def public_auth_url(self) -> str:
-        """Return the public_auth_url."""
-        return self.get_remote_app_data('public-auth-url')
-
-    @property
-    def admin_role(self) -> str:
-        """Return the admin_role."""
-        return self.get_remote_app_data('admin-role')
-
-    def register_services(self, service_endpoints: dict,
-                          region: str) -> None:
-        """Request access to the IdentityService server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting service registration")
-            app_data = self._identity_service_rel.data[self.charm.app]
-            app_data["service-endpoints"] = json.dumps(
-                service_endpoints, sort_keys=True
-            )
-            app_data["region"] = region
-
-
-class HasIdentityServiceClientsEvent(EventBase):
-    """Has IdentityServiceClients Event."""
-
-    pass
-
-
-class ReadyIdentityServiceClientsEvent(EventBase):
-    """IdentityServiceClients Ready Event."""
-
-    def __init__(self, handle, relation_id, relation_name, service_endpoints,
-                 region, client_app_name):
-        super().__init__(handle)
-        self.relation_id = relation_id
-        self.relation_name = relation_name
-        self.service_endpoints = service_endpoints
-        self.region = region
-        self.client_app_name = client_app_name
-
-    def snapshot(self):
-        return {
-            "relation_id": self.relation_id,
-            "relation_name": self.relation_name,
-            "service_endpoints": self.service_endpoints,
-            "client_app_name": self.client_app_name,
-            "region": self.region}
-
-    def restore(self, snapshot):
-        super().restore(snapshot)
-        self.relation_id = snapshot["relation_id"]
-        self.relation_name = snapshot["relation_name"]
-        self.service_endpoints = snapshot["service_endpoints"]
-        self.region = snapshot["region"]
-        self.client_app_name = snapshot["client_app_name"]
-
-
-class IdentityServiceClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
-    ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
-
-
-class IdentityServiceProvides(Object):
-    """
-    IdentityServiceProvides class
-    """
-
-    on = IdentityServiceClientEvents()
-    _stored = StoredState()
-
-    def __init__(self, charm, relation_name):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_identity_service_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_identity_service_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_identity_service_relation_broken,
-        )
-
-    def _on_identity_service_relation_joined(self, event):
-        """Handle IdentityService joined."""
-        logging.debug("IdentityService on_joined")
-        self.on.has_identity_service_clients.emit()
-
-    def _on_identity_service_relation_changed(self, event):
-        """Handle IdentityService changed."""
-        logging.debug("IdentityService on_changed")
-        REQUIRED_KEYS = [
-            'service-endpoints',
-            'region']
-
-        values = [
-            event.relation.data[event.relation.app].get(k)
-            for k in REQUIRED_KEYS
-        ]
-        # Validate data on the relation
-        if all(values):
-            service_eps = json.loads(
-                event.relation.data[event.relation.app]['service-endpoints'])
-            self.on.ready_identity_service_clients.emit(
-                event.relation.id,
-                event.relation.name,
-                service_eps,
-                event.relation.data[event.relation.app]['region'],
-                event.relation.app.name)
-
-    def _on_identity_service_relation_broken(self, event):
-        """Handle IdentityService broken."""
-        logging.debug("IdentityServiceProvides on_departed")
-        # TODO clear data on the relation
-
-    def set_identity_service_credentials(self, relation_name: int,
-                                         relation_id: str,
-                                         api_version: str,
-                                         auth_host: str,
-                                         auth_port: str,
-                                         auth_protocol: str,
-                                         internal_host: str,
-                                         internal_port: str,
-                                         internal_protocol: str,
-                                         service_host: str,
-                                         service_port: str,
-                                         service_protocol: str,
-                                         admin_domain: str,
-                                         admin_project: str,
-                                         admin_user: str,
-                                         service_domain: str,
-                                         service_project: str,
-                                         service_user: str,
-                                         internal_auth_url: str,
-                                         admin_auth_url: str,
-                                         public_auth_url: str,
-                                         service_credentials: str,
-                                         admin_role: str):
-        logging.debug("Setting identity_service connection information.")
-        _identity_service_rel = None
-        for relation in self.framework.model.relations[relation_name]:
-            if relation.id == relation_id:
-                _identity_service_rel = relation
-        if not _identity_service_rel:
-            # Relation has disappeared so skip send of data
-            return
-        app_data = _identity_service_rel.data[self.charm.app]
-        app_data["api-version"] = api_version
-        app_data["auth-host"] = auth_host
-        app_data["auth-port"] = str(auth_port)
-        app_data["auth-protocol"] = auth_protocol
-        app_data["internal-host"] = internal_host
-        app_data["internal-port"] = str(internal_port)
-        app_data["internal-protocol"] = internal_protocol
-        app_data["service-host"] = service_host
-        app_data["service-port"] = str(service_port)
-        app_data["service-protocol"] = service_protocol
-        app_data["admin-domain-name"] = admin_domain.name
-        app_data["admin-domain-id"] = admin_domain.id
-        app_data["admin-project-name"] = admin_project.name
-        app_data["admin-project-id"] = admin_project.id
-        app_data["admin-user-name"] = admin_user.name
-        app_data["admin-user-id"] = admin_user.id
-        app_data["service-domain-name"] = service_domain.name
-        app_data["service-domain-id"] = service_domain.id
-        app_data["service-project-name"] = service_project.name
-        app_data["service-project-id"] = service_project.id
-        app_data["service-user-id"] = service_user.id
-        app_data["internal-auth-url"] = internal_auth_url
-        app_data["admin-auth-url"] = admin_auth_url
-        app_data["public-auth-url"] = public_auth_url
-        app_data["service-credentials"] = service_credentials
-        app_data["admin-role"] = admin_role
diff --git a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
deleted file mode 100644
index c7df2409..00000000
--- a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
+++ /dev/null
@@ -1,286 +0,0 @@
-"""RabbitMQProvides and Requires module.
-
-This library contains the Requires and Provides classes for handling
-the rabbitmq interface.
-
-Import `RabbitMQRequires` in your charm, with the charm object and the
-relation name:
-    - self
-    - "amqp"
-
-Also provide two additional parameters to the charm object:
-    - username
-    - vhost
-
-Two events are also available to respond to:
-    - connected
-    - ready
-    - goneaway
-
-A basic example showing the usage of this relation follows:
-
-```
-from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
-
-class RabbitMQClientCharm(CharmBase):
-    def __init__(self, *args):
-        super().__init__(*args)
-        # RabbitMQ Requires
-        self.amqp = RabbitMQRequires(
-            self, "amqp",
-            username="myusername",
-            vhost="vhostname"
-        )
-        self.framework.observe(
-            self.amqp.on.connected, self._on_amqp_connected)
-        self.framework.observe(
-            self.amqp.on.ready, self._on_amqp_ready)
-        self.framework.observe(
-            self.amqp.on.goneaway, self._on_amqp_goneaway)
-
-    def _on_amqp_connected(self, event):
-        '''React to the RabbitMQ connected event.
-
-        This event happens when n RabbitMQ relation is added to the
-        model before credentials etc have been provided.
-        '''
-        # Do something before the relation is complete
-        pass
-
-    def _on_amqp_ready(self, event):
-        '''React to the RabbitMQ ready event.
-
-        The RabbitMQ interface will use the provided username and vhost for the
-        request to the rabbitmq server.
-        '''
-        # RabbitMQ Relation is ready. Do something with the completed relation.
-        pass
-
-    def _on_amqp_goneaway(self, event):
-        '''React to the RabbitMQ goneaway event.
-
-        This event happens when an RabbitMQ relation is removed.
-        '''
-        # RabbitMQ Relation has goneaway. shutdown services or suchlike
-        pass
-```
-"""
-
-# The unique Charmhub library identifier, never change it
-LIBID = "45622352791142fd9cf87232e3bd6f2a"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 0
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 1
-
-import logging
-
-from ops.framework import (
-    StoredState,
-    EventBase,
-    ObjectEvents,
-    EventSource,
-    Object,
-)
-
-from ops.model import Relation
-
-from typing import List
-
-logger = logging.getLogger(__name__)
-
-
-class RabbitMQConnectedEvent(EventBase):
-    """RabbitMQ connected Event."""
-
-    pass
-
-
-class RabbitMQReadyEvent(EventBase):
-    """RabbitMQ ready for use Event."""
-
-    pass
-
-
-class RabbitMQGoneAwayEvent(EventBase):
-    """RabbitMQ relation has gone-away Event"""
-
-    pass
-
-
-class RabbitMQServerEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    connected = EventSource(RabbitMQConnectedEvent)
-    ready = EventSource(RabbitMQReadyEvent)
-    goneaway = EventSource(RabbitMQGoneAwayEvent)
-
-
-class RabbitMQRequires(Object):
-    """
-    RabbitMQRequires class
-    """
-
-    on = RabbitMQServerEvents()
-
-    def __init__(self, charm, relation_name: str, username: str, vhost: str):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.username = username
-        self.vhost = vhost
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_departed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """RabbitMQ relation joined."""
-        logging.debug("RabbitMQRabbitMQRequires on_joined")
-        self.on.connected.emit()
-        self.request_access(self.username, self.vhost)
-
-    def _on_amqp_relation_changed(self, event):
-        """RabbitMQ relation changed."""
-        logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
-        if self.password:
-            self.on.ready.emit()
-
-    def _on_amqp_relation_broken(self, event):
-        """RabbitMQ relation broken."""
-        logging.debug("RabbitMQRabbitMQRequires on_broken")
-        self.on.goneaway.emit()
-
-    @property
-    def _amqp_rel(self) -> Relation:
-        """The RabbitMQ relation."""
-        return self.framework.model.get_relation(self.relation_name)
-
-    @property
-    def password(self) -> str:
-        """Return the RabbitMQ password from the server side of the relation."""
-        return self._amqp_rel.data[self._amqp_rel.app].get("password")
-
-    @property
-    def hostname(self) -> str:
-        """Return the hostname from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
-
-    @property
-    def ssl_port(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
-
-    @property
-    def ssl_ca(self) -> str:
-        """Return the SSL port from the RabbitMQ relation"""
-        return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
-
-    @property
-    def hostnames(self) -> List[str]:
-        """Return a list of remote RMQ hosts from the RabbitMQ relation"""
-        _hosts = []
-        for unit in self._amqp_rel.units:
-            _hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
-        return _hosts
-
-    def request_access(self, username: str, vhost: str) -> None:
-        """Request access to the RabbitMQ server."""
-        if self.model.unit.is_leader():
-            logging.debug("Requesting RabbitMQ user and vhost")
-            self._amqp_rel.data[self.charm.app]["username"] = username
-            self._amqp_rel.data[self.charm.app]["vhost"] = vhost
-
-
-class HasRabbitMQClientsEvent(EventBase):
-    """Has RabbitMQClients Event."""
-
-    pass
-
-
-class ReadyRabbitMQClientsEvent(EventBase):
-    """RabbitMQClients Ready Event."""
-
-    pass
-
-
-class RabbitMQClientEvents(ObjectEvents):
-    """Events class for `on`"""
-
-    has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
-    ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
-
-
-class RabbitMQProvides(Object):
-    """
-    RabbitMQProvides class
-    """
-
-    on = RabbitMQClientEvents()
-
-    def __init__(self, charm, relation_name, callback):
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.relation_name = relation_name
-        self.callback = callback
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined,
-            self._on_amqp_relation_joined,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed,
-            self._on_amqp_relation_changed,
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_broken,
-            self._on_amqp_relation_broken,
-        )
-
-    def _on_amqp_relation_joined(self, event):
-        """Handle RabbitMQ joined."""
-        logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
-                      .format(event.relation.data[event.relation.app]))
-        self.on.has_amqp_clients.emit()
-
-    def _on_amqp_relation_changed(self, event):
-        """Handle RabbitMQ changed."""
-        logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
-                      .format(event.relation.data[event.relation.app]))
-        # Validate data on the relation
-        if self.username(event) and self.vhost(event):
-            self.on.ready_amqp_clients.emit()
-            if self.charm.unit.is_leader():
-                self.callback(event, self.username(event), self.vhost(event))
-        else:
-            logging.warning("Received RabbitMQ changed event without the "
-                            "expected keys ('username', 'vhost') in the "
-                            "application data bag.  Incompatible charm in "
-                            "other end of relation?")
-
-    def _on_amqp_relation_broken(self, event):
-        """Handle RabbitMQ broken."""
-        logging.debug("RabbitMQRabbitMQProvides on_departed")
-        # TODO clear data on the relation
-
-    def username(self, event):
-        """Return the RabbitMQ username from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("username")
-
-    def vhost(self, event):
-        """Return the RabbitMQ vhost from the client side of the relation."""
-        return event.relation.data[event.relation.app].get("vhost")
diff --git a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py
deleted file mode 100644
index 0364c8ab..00000000
--- a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py
+++ /dev/null
@@ -1,734 +0,0 @@
-# Copyright 2023 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-r"""# Interface Library for ingress.
-
-This library wraps relation endpoints using the `ingress` interface
-and provides a Python API for both requesting and providing per-application
-ingress, with load-balancing occurring across all units.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`.
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.traefik_k8s.v1.ingress
-```
-
-In the `metadata.yaml` of the charm, add the following:
-
-```yaml
-requires:
-    ingress:
-        interface: ingress
-        limit: 1
-```
-
-Then, to initialise the library:
-
-```python
-from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
-  IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.ingress = IngressPerAppRequirer(self, port=80)
-    # The following event is triggered when the ingress URL to be used
-    # by this deployment of the `SomeCharm` is ready (or changes).
-    self.framework.observe(
-        self.ingress.on.ready, self._on_ingress_ready
-    )
-    self.framework.observe(
-        self.ingress.on.revoked, self._on_ingress_revoked
-    )
-
-    def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
-        logger.info("This app's ingress URL: %s", event.url)
-
-    def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
-        logger.info("This app no longer has ingress")
-"""
-import json
-import logging
-import socket
-import typing
-from dataclasses import dataclass
-from typing import (
-    Any,
-    Dict,
-    List,
-    MutableMapping,
-    Optional,
-    Sequence,
-    Tuple,
-)
-
-import pydantic
-from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
-from ops.framework import EventSource, Object, ObjectEvents, StoredState
-from ops.model import ModelError, Relation, Unit
-from pydantic import AnyHttpUrl, BaseModel, Field, validator
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
-
-# Increment this major API version when introducing breaking changes
-LIBAPI = 2
-
-# Increment this PATCH version before using `charmcraft publish-lib` or reset
-# to 0 if you are raising the major API version
-LIBPATCH = 6
-
-PYDEPS = ["pydantic<2.0"]
-
-DEFAULT_RELATION_NAME = "ingress"
-RELATION_INTERFACE = "ingress"
-
-log = logging.getLogger(__name__)
-BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
-
-
-class DatabagModel(BaseModel):
-    """Base databag model."""
-
-    class Config:
-        """Pydantic config."""
-
-        allow_population_by_field_name = True
-        """Allow instantiating this class by field name (instead of forcing alias)."""
-
-    _NEST_UNDER = None
-
-    @classmethod
-    def load(cls, databag: MutableMapping):
-        """Load this model from a Juju databag."""
-        if cls._NEST_UNDER:
-            return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
-
-        try:
-            data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
-        except json.JSONDecodeError as e:
-            msg = f"invalid databag contents: expecting json. {databag}"
-            log.error(msg)
-            raise DataValidationError(msg) from e
-
-        try:
-            return cls.parse_raw(json.dumps(data))  # type: ignore
-        except pydantic.ValidationError as e:
-            msg = f"failed to validate databag: {databag}"
-            log.error(msg, exc_info=True)
-            raise DataValidationError(msg) from e
-
-    def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
-        """Write the contents of this model to Juju databag.
-
-        :param databag: the databag to write the data to.
-        :param clear: ensure the databag is cleared before writing it.
-        """
-        if clear and databag:
-            databag.clear()
-
-        if databag is None:
-            databag = {}
-
-        if self._NEST_UNDER:
-            databag[self._NEST_UNDER] = self.json()
-
-        dct = self.dict()
-        for key, field in self.__fields__.items():  # type: ignore
-            value = dct[key]
-            databag[field.alias or key] = json.dumps(value)
-
-        return databag
-
-
-# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
-class IngressUrl(BaseModel):
-    """Ingress url schema."""
-
-    url: AnyHttpUrl
-
-
-class IngressProviderAppData(DatabagModel):
-    """Ingress application databag schema."""
-
-    ingress: IngressUrl
-
-
-class ProviderSchema(BaseModel):
-    """Provider schema for Ingress."""
-
-    app: IngressProviderAppData
-
-
-class IngressRequirerAppData(DatabagModel):
-    """Ingress requirer application databag model."""
-
-    model: str = Field(description="The model the application is in.")
-    name: str = Field(description="the name of the app requesting ingress.")
-    port: int = Field(description="The port the app wishes to be exposed.")
-
-    # fields on top of vanilla 'ingress' interface:
-    strip_prefix: Optional[bool] = Field(
-        description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
-    )
-    redirect_https: Optional[bool] = Field(
-        description="Whether to redirect http traffic to https.", alias="redirect-https"
-    )
-
-    scheme: Optional[str] = Field(
-        default="http", description="What scheme to use in the generated ingress url"
-    )
-
-    @validator("scheme", pre=True)
-    def validate_scheme(cls, scheme):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate scheme arg."""
-        if scheme not in {"http", "https", "h2c"}:
-            raise ValueError("invalid scheme: should be one of `http|https|h2c`")
-        return scheme
-
-    @validator("port", pre=True)
-    def validate_port(cls, port):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate port."""
-        assert isinstance(port, int), type(port)
-        assert 0 < port < 65535, "port out of TCP range"
-        return port
-
-
-class IngressRequirerUnitData(DatabagModel):
-    """Ingress requirer unit databag model."""
-
-    host: str = Field(description="Hostname the unit wishes to be exposed.")
-
-    @validator("host", pre=True)
-    def validate_host(cls, host):  # noqa: N805  # pydantic wants 'cls' as first arg
-        """Validate host."""
-        assert isinstance(host, str), type(host)
-        return host
-
-
-class RequirerSchema(BaseModel):
-    """Requirer schema for Ingress."""
-
-    app: IngressRequirerAppData
-    unit: IngressRequirerUnitData
-
-
-class IngressError(RuntimeError):
-    """Base class for custom errors raised by this library."""
-
-
-class NotReadyError(IngressError):
-    """Raised when a relation is not ready."""
-
-
-class DataValidationError(IngressError):
-    """Raised when data validation fails on IPU relation data."""
-
-
-class _IngressPerAppBase(Object):
-    """Base class for IngressPerUnit interface classes."""
-
-    def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
-        super().__init__(charm, relation_name)
-
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self.app = self.charm.app
-        self.unit = self.charm.unit
-
-        observe = self.framework.observe
-        rel_events = charm.on[relation_name]
-        observe(rel_events.relation_created, self._handle_relation)
-        observe(rel_events.relation_joined, self._handle_relation)
-        observe(rel_events.relation_changed, self._handle_relation)
-        observe(rel_events.relation_broken, self._handle_relation_broken)
-        observe(charm.on.leader_elected, self._handle_upgrade_or_leader)  # type: ignore
-        observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader)  # type: ignore
-
-    @property
-    def relations(self):
-        """The list of Relation instances associated with this endpoint."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def _handle_relation(self, event):
-        """Subclasses should implement this method to handle a relation update."""
-        pass
-
-    def _handle_relation_broken(self, event):
-        """Subclasses should implement this method to handle a relation breaking."""
-        pass
-
-    def _handle_upgrade_or_leader(self, event):
-        """Subclasses should implement this method to handle upgrades or leadership change."""
-        pass
-
-
-class _IPAEvent(RelationEvent):
-    __args__: Tuple[str, ...] = ()
-    __optional_kwargs__: Dict[str, Any] = {}
-
-    @classmethod
-    def __attrs__(cls):
-        return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
-
-    def __init__(self, handle, relation, *args, **kwargs):
-        super().__init__(handle, relation)
-
-        if not len(self.__args__) == len(args):
-            raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
-
-        for attr, obj in zip(self.__args__, args):
-            setattr(self, attr, obj)
-        for attr, default in self.__optional_kwargs__.items():
-            obj = kwargs.get(attr, default)
-            setattr(self, attr, obj)
-
-    def snapshot(self):
-        dct = super().snapshot()
-        for attr in self.__attrs__():
-            obj = getattr(self, attr)
-            try:
-                dct[attr] = obj
-            except ValueError as e:
-                raise ValueError(
-                    "cannot automagically serialize {}: "
-                    "override this method and do it "
-                    "manually.".format(obj)
-                ) from e
-
-        return dct
-
-    def restore(self, snapshot) -> None:
-        super().restore(snapshot)
-        for attr, obj in snapshot.items():
-            setattr(self, attr, obj)
-
-
-class IngressPerAppDataProvidedEvent(_IPAEvent):
-    """Event representing that ingress data has been provided for an app."""
-
-    __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
-
-    if typing.TYPE_CHECKING:
-        name: Optional[str] = None
-        model: Optional[str] = None
-        # sequence of hostname, port dicts
-        hosts: Sequence["IngressRequirerUnitData"] = ()
-        strip_prefix: bool = False
-        redirect_https: bool = False
-
-
-class IngressPerAppDataRemovedEvent(RelationEvent):
-    """Event representing that ingress data has been removed for an app."""
-
-
-class IngressPerAppProviderEvents(ObjectEvents):
-    """Container for IPA Provider events."""
-
-    data_provided = EventSource(IngressPerAppDataProvidedEvent)
-    data_removed = EventSource(IngressPerAppDataRemovedEvent)
-
-
-@dataclass
-class IngressRequirerData:
-    """Data exposed by the ingress requirer to the provider."""
-
-    app: "IngressRequirerAppData"
-    units: List["IngressRequirerUnitData"]
-
-
-class TlsProviderType(typing.Protocol):
-    """Placeholder."""
-
-    @property
-    def enabled(self) -> bool:  # type: ignore
-        """Placeholder."""
-
-
-class IngressPerAppProvider(_IngressPerAppBase):
-    """Implementation of the provider of ingress."""
-
-    on = IngressPerAppProviderEvents()  # type: ignore
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-    ):
-        """Constructor for IngressPerAppProvider.
-
-        Args:
-            charm: The charm that is instantiating the instance.
-            relation_name: The name of the relation endpoint to bind to
-                (defaults to "ingress").
-        """
-        super().__init__(charm, relation_name)
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if remote side has sent the required data:
-        # notify listeners.
-        if self.is_ready(event.relation):
-            data = self.get_data(event.relation)
-            self.on.data_provided.emit(  # type: ignore
-                event.relation,
-                data.app.name,
-                data.app.model,
-                [unit.dict() for unit in data.units],
-                data.app.strip_prefix or False,
-                data.app.redirect_https or False,
-            )
-
-    def _handle_relation_broken(self, event):
-        self.on.data_removed.emit(event.relation)  # type: ignore
-
-    def wipe_ingress_data(self, relation: Relation):
-        """Clear ingress data from relation."""
-        assert self.unit.is_leader(), "only leaders can do this"
-        try:
-            relation.data
-        except ModelError as e:
-            log.warning(
-                "error {} accessing relation data for {!r}. "
-                "Probably a ghost of a dead relation is still "
-                "lingering around.".format(e, relation.name)
-            )
-            return
-        del relation.data[self.app]["ingress"]
-
-    def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
-        """Fetch and validate the requirer's app databag."""
-        out: List["IngressRequirerUnitData"] = []
-
-        unit: Unit
-        for unit in relation.units:
-            databag = relation.data[unit]
-            try:
-                data = IngressRequirerUnitData.load(databag)
-                out.append(data)
-            except pydantic.ValidationError:
-                log.info(f"failed to validate remote unit data for {unit}")
-                raise
-        return out
-
-    @staticmethod
-    def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
-        """Fetch and validate the requirer's app databag."""
-        app = relation.app
-        if app is None:
-            raise NotReadyError(relation)
-
-        databag = relation.data[app]
-        return IngressRequirerAppData.load(databag)
-
-    def get_data(self, relation: Relation) -> IngressRequirerData:
-        """Fetch the remote (requirer) app and units' databags."""
-        try:
-            return IngressRequirerData(
-                self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
-            )
-        except (pydantic.ValidationError, DataValidationError) as e:
-            raise DataValidationError("failed to validate ingress requirer data") from e
-
-    def is_ready(self, relation: Optional[Relation] = None):
-        """The Provider is ready if the requirer has sent valid data."""
-        if not relation:
-            return any(map(self.is_ready, self.relations))
-
-        try:
-            self.get_data(relation)
-        except (DataValidationError, NotReadyError) as e:
-            log.debug("Provider not ready; validation error encountered: %s" % str(e))
-            return False
-        return True
-
-    def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
-        """Fetch and validate this app databag; return the ingress url."""
-        if not self.is_ready(relation) or not self.unit.is_leader():
-            # Handle edge case where remote app name can be missing, e.g.,
-            # relation_broken events.
-            # Also, only leader units can read own app databags.
-            # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
-            return None
-
-        # fetch the provider's app databag
-        databag = relation.data[self.app]
-        if not databag.get("ingress"):
-            raise NotReadyError("This application did not `publish_url` yet.")
-
-        return IngressProviderAppData.load(databag)
-
-    def publish_url(self, relation: Relation, url: str):
-        """Publish to the app databag the ingress url."""
-        ingress_url = {"url": url}
-        IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
-
-    @property
-    def proxied_endpoints(self) -> Dict[str, str]:
-        """Returns the ingress settings provided to applications by this IngressPerAppProvider.
-
-        For example, when this IngressPerAppProvider has provided the
-        `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
-        will be:
-
-        ```
-        {
-            "my-app": {
-                "url": "http://foo.bar/my-model.my-app"
-            }
-        }
-        ```
-        """
-        results = {}
-
-        for ingress_relation in self.relations:
-            if not ingress_relation.app:
-                log.warning(
-                    f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
-                )
-                continue
-            try:
-                ingress_data = self._published_url(ingress_relation)
-            except NotReadyError:
-                log.warning(
-                    f"no published url found in {ingress_relation}: "
-                    f"traefik didn't publish_url yet to this relation."
-                )
-                continue
-
-            if not ingress_data:
-                log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
-                continue
-
-            results[ingress_relation.app.name] = ingress_data.ingress.dict()
-        return results
-
-
-class IngressPerAppReadyEvent(_IPAEvent):
-    """Event representing that ingress for an app is ready."""
-
-    __args__ = ("url",)
-    if typing.TYPE_CHECKING:
-        url: Optional[str] = None
-
-
-class IngressPerAppRevokedEvent(RelationEvent):
-    """Event representing that ingress for an app has been revoked."""
-
-
-class IngressPerAppRequirerEvents(ObjectEvents):
-    """Container for IPA Requirer events."""
-
-    ready = EventSource(IngressPerAppReadyEvent)
-    revoked = EventSource(IngressPerAppRevokedEvent)
-
-
-class IngressPerAppRequirer(_IngressPerAppBase):
-    """Implementation of the requirer of the ingress relation."""
-
-    on = IngressPerAppRequirerEvents()  # type: ignore
-
-    # used to prevent spurious urls to be sent out if the event we're currently
-    # handling is a relation-broken one.
-    _stored = StoredState()
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        relation_name: str = DEFAULT_RELATION_NAME,
-        *,
-        host: Optional[str] = None,
-        port: Optional[int] = None,
-        strip_prefix: bool = False,
-        redirect_https: bool = False,
-        # fixme: this is horrible UX.
-        #  shall we switch to manually calling provide_ingress_requirements with all args when ready?
-        scheme: typing.Callable[[], str] = lambda: "http",
-    ):
-        """Constructor for IngressRequirer.
-
-        The request args can be used to specify the ingress properties when the
-        instance is created. If any are set, at least `port` is required, and
-        they will be sent to the ingress provider as soon as it is available.
-        All request args must be given as keyword args.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
-                relation must be of interface type `ingress` and have "limit: 1")
-            host: Hostname to be used by the ingress provider to address the requiring
-                application; if unspecified, the default Kubernetes service name will be used.
-            strip_prefix: configure Traefik to strip the path prefix.
-            redirect_https: redirect incoming requests to HTTPS.
-            scheme: callable returning the scheme to use when constructing the ingress url.
-
-        Request Args:
-            port: the port of the service
-        """
-        super().__init__(charm, relation_name)
-        self.charm: CharmBase = charm
-        self.relation_name = relation_name
-        self._strip_prefix = strip_prefix
-        self._redirect_https = redirect_https
-        self._get_scheme = scheme
-
-        self._stored.set_default(current_url=None)  # type: ignore
-
-        # if instantiated with a port, and we are related, then
-        # we immediately publish our ingress data  to speed up the process.
-        if port:
-            self._auto_data = host, port
-        else:
-            self._auto_data = None
-
-    def _handle_relation(self, event):
-        # created, joined or changed: if we have auto data: publish it
-        self._publish_auto_data()
-
-        if self.is_ready():
-            # Avoid spurious events, emit only when there is a NEW URL available
-            new_url = (
-                None
-                if isinstance(event, RelationBrokenEvent)
-                else self._get_url_from_relation_data()
-            )
-            if self._stored.current_url != new_url:  # type: ignore
-                self._stored.current_url = new_url  # type: ignore
-                self.on.ready.emit(event.relation, new_url)  # type: ignore
-
-    def _handle_relation_broken(self, event):
-        self._stored.current_url = None  # type: ignore
-        self.on.revoked.emit(event.relation)  # type: ignore
-
-    def _handle_upgrade_or_leader(self, event):
-        """On upgrade/leadership change: ensure we publish the data we have."""
-        self._publish_auto_data()
-
-    def is_ready(self):
-        """The Requirer is ready if the Provider has sent valid data."""
-        try:
-            return bool(self._get_url_from_relation_data())
-        except DataValidationError as e:
-            log.debug("Requirer not ready; validation error encountered: %s" % str(e))
-            return False
-
-    def _publish_auto_data(self):
-        if self._auto_data:
-            host, port = self._auto_data
-            self.provide_ingress_requirements(host=host, port=port)
-
-    def provide_ingress_requirements(
-        self,
-        *,
-        scheme: Optional[str] = None,
-        host: Optional[str] = None,
-        port: int,
-    ):
-        """Publishes the data that Traefik needs to provide ingress.
-
-        Args:
-            scheme: Scheme to be used; if unspecified, use the one used by __init__.
-            host: Hostname to be used by the ingress provider to address the
-             requirer unit; if unspecified, FQDN will be used instead
-            port: the port of the service (required)
-        """
-        for relation in self.relations:
-            self._provide_ingress_requirements(scheme, host, port, relation)
-
-    def _provide_ingress_requirements(
-        self,
-        scheme: Optional[str],
-        host: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        if self.unit.is_leader():
-            self._publish_app_data(scheme, port, relation)
-
-        self._publish_unit_data(host, relation)
-
-    def _publish_unit_data(
-        self,
-        host: Optional[str],
-        relation: Relation,
-    ):
-        if not host:
-            host = socket.getfqdn()
-
-        unit_databag = relation.data[self.unit]
-        try:
-            IngressRequirerUnitData(host=host).dump(unit_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate unit data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    def _publish_app_data(
-        self,
-        scheme: Optional[str],
-        port: int,
-        relation: Relation,
-    ):
-        # assumes leadership!
-        app_databag = relation.data[self.app]
-
-        if not scheme:
-            # If scheme was not provided, use the one given to the constructor.
-            scheme = self._get_scheme()
-
-        try:
-            IngressRequirerAppData(  # type: ignore  # pyright does not like aliases
-                model=self.model.name,
-                name=self.app.name,
-                scheme=scheme,
-                port=port,
-                strip_prefix=self._strip_prefix,  # type: ignore  # pyright does not like aliases
-                redirect_https=self._redirect_https,  # type: ignore  # pyright does not like aliases
-            ).dump(app_databag)
-        except pydantic.ValidationError as e:
-            msg = "failed to validate app data"
-            log.info(msg, exc_info=True)  # log to INFO because this might be expected
-            raise DataValidationError(msg) from e
-
-    @property
-    def relation(self):
-        """The established Relation instance, or None."""
-        return self.relations[0] if self.relations else None
-
-    def _get_url_from_relation_data(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        relation = self.relation
-        if not relation or not relation.app:
-            return None
-
-        # fetch the provider's app databag
-        try:
-            databag = relation.data[relation.app]
-        except ModelError as e:
-            log.debug(
-                f"Error {e} attempting to read remote app data; "
-                f"probably we are in a relation_departed hook"
-            )
-            return None
-
-        if not databag:  # not ready yet
-            return None
-
-        return str(IngressProviderAppData.load(databag).ingress.url)
-
-    @property
-    def url(self) -> Optional[str]:
-        """The full ingress URL to reach the current unit.
-
-        Returns None if the URL isn't available yet.
-        """
-        data = (
-            typing.cast(Optional[str], self._stored.current_url)  # type: ignore
-            or self._get_url_from_relation_data()
-        )
-        return data
diff --git a/charms/placement-k8s/osci.yaml b/charms/placement-k8s/osci.yaml
deleted file mode 100644
index 60172b1c..00000000
--- a/charms/placement-k8s/osci.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- project:
-    templates:
-      - charm-publish-jobs
-    vars:
-      needs_charm_build: true
-      charm_build_name: placement-k8s
-      build_type: charmcraft
-      publish_charm: true
-      charmcraft_channel: 2.0/stable
-      publish_channel: 2023.2/edge
diff --git a/charms/placement-k8s/pyproject.toml b/charms/placement-k8s/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/charms/placement-k8s/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/charms/placement-k8s/rename.sh b/charms/placement-k8s/rename.sh
deleted file mode 100755
index d0c35c97..00000000
--- a/charms/placement-k8s/rename.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
-echo "renaming ${charm}_*.charm to ${charm}.charm"
-echo -n "pwd: "
-pwd
-ls -al
-echo "Removing bad downloaded charm maybe?"
-if [[ -e "${charm}.charm" ]];
-then
-    rm "${charm}.charm"
-fi
-echo "Renaming charm here."
-mv ${charm}_*.charm ${charm}.charm
diff --git a/charms/placement-k8s/requirements.txt b/charms/placement-k8s/requirements.txt
index e988c61d..64399ba9 100644
--- a/charms/placement-k8s/requirements.txt
+++ b/charms/placement-k8s/requirements.txt
@@ -12,4 +12,5 @@ lightkube
 lightkube-models
 ops
 
-git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+# From ops_sunbeam
+tenacity
diff --git a/charms/placement-k8s/src/templates/parts/section-database b/charms/placement-k8s/src/templates/parts/section-database
deleted file mode 100644
index eb52f65e..00000000
--- a/charms/placement-k8s/src/templates/parts/section-database
+++ /dev/null
@@ -1,7 +0,0 @@
-[database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/cinder/cinder.db
-{% endif -%}
-connection_recycle_time = 200
diff --git a/charms/placement-k8s/src/templates/parts/section-federation b/charms/placement-k8s/src/templates/parts/section-federation
deleted file mode 100644
index 65ee99ed..00000000
--- a/charms/placement-k8s/src/templates/parts/section-federation
+++ /dev/null
@@ -1,10 +0,0 @@
-{% if trusted_dashboards %}
-[federation]
-{% for dashboard_url in trusted_dashboards -%}
-trusted_dashboard = {{ dashboard_url }}
-{% endfor -%}
-{% endif %}
-{% for sp in fid_sps -%}
-[{{ sp['protocol-name'] }}]
-remote_id_attribute = {{ sp['remote-id-attribute'] }}
-{% endfor -%}
diff --git a/charms/placement-k8s/src/templates/parts/section-identity b/charms/placement-k8s/src/templates/parts/section-identity
deleted file mode 100644
index cbb1d069..00000000
--- a/charms/placement-k8s/src/templates/parts/section-identity
+++ /dev/null
@@ -1,24 +0,0 @@
-[keystone_authtoken]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-interface = admin
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-interface = internal
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-interface = internal
-{% endif -%}
-{% if identity_service.public_auth_url -%}
-www_authenticate_uri = {{ identity_service.public_auth_url }}
-{% elif identity_service.internal_host -%}
-www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-auth_type = password
-project_domain_name = {{ identity_service.service_domain_name }}
-user_domain_name = {{ identity_service.service_domain_name }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-service_token_roles = {{ identity_service.admin_role }}
-service_token_roles_required = True
diff --git a/charms/placement-k8s/src/templates/parts/section-middleware b/charms/placement-k8s/src/templates/parts/section-middleware
deleted file mode 100644
index e65f1d98..00000000
--- a/charms/placement-k8s/src/templates/parts/section-middleware
+++ /dev/null
@@ -1,6 +0,0 @@
-{% for section in sections -%}
-[{{section}}]
-{% for key, value in sections[section].items() -%}
-{{ key }} = {{ value }}
-{% endfor %}
-{%- endfor %}
diff --git a/charms/placement-k8s/src/templates/parts/section-service-user b/charms/placement-k8s/src/templates/parts/section-service-user
deleted file mode 100644
index 65103693..00000000
--- a/charms/placement-k8s/src/templates/parts/section-service-user
+++ /dev/null
@@ -1,17 +0,0 @@
-{% if identity_service.service_domain_id -%}
-[service_user]
-{% if identity_service.admin_auth_url -%}
-auth_url = {{ identity_service.admin_auth_url }}
-{% elif identity_service.internal_auth_url -%}
-auth_url = {{ identity_service.internal_auth_url }}
-{% elif identity_service.internal_host -%}
-auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
-{% endif -%}
-send_service_user_token = true
-auth_type = password
-project_domain_id = {{ identity_service.service_domain_id }}
-user_domain_id = {{ identity_service.service_domain_id }}
-project_name = {{ identity_service.service_project_name }}
-username = {{ identity_service.service_user_name }}
-password = {{ identity_service.service_password }}
-{% endif -%}
diff --git a/charms/placement-k8s/src/templates/parts/section-signing b/charms/placement-k8s/src/templates/parts/section-signing
deleted file mode 100644
index cb7d69ae..00000000
--- a/charms/placement-k8s/src/templates/parts/section-signing
+++ /dev/null
@@ -1,15 +0,0 @@
-{% if enable_signing -%}
-[signing]
-{% if certfile -%}
-certfile = {{ certfile }}
-{% endif -%}
-{% if keyfile -%}
-keyfile = {{ keyfile }}
-{% endif -%}
-{% if ca_certs -%}
-ca_certs = {{ ca_certs }}
-{% endif -%}
-{% if ca_key -%}
-ca_key = {{ ca_key }}
-{% endif -%}
-{% endif -%}
\ No newline at end of file
diff --git a/charms/placement-k8s/src/templates/placement.conf b/charms/placement-k8s/src/templates/placement.conf
index c8f3673c..771165cf 100644
--- a/charms/placement-k8s/src/templates/placement.conf
+++ b/charms/placement-k8s/src/templates/placement.conf
@@ -5,11 +5,7 @@ debug = {{ options.debug }}
 auth_strategy = keystone
 
 [placement_database]
-{% if database.connection -%}
-connection = {{ database.connection }}
-{% else -%}
-connection = sqlite:////var/lib/placement/placement.db
-{% endif -%}
+{% include "parts/database-connection" %}
 
 {% include "parts/section-identity" %}
 
diff --git a/charms/placement-k8s/test-requirements.txt b/charms/placement-k8s/test-requirements.txt
deleted file mode 100644
index a9b0d698..00000000
--- a/charms/placement-k8s/test-requirements.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of *requirements.txt files for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-#
-
-coverage
-mock
-flake8
-stestr
-git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
-git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
-git+https://opendev.org/openstack/tempest.git#egg=tempest
-ops
-# Subunit 1.4.3+ requires extras
-extras
diff --git a/charms/placement-k8s/tests/unit/test_placement_charm.py b/charms/placement-k8s/tests/unit/test_placement_charm.py
index 21ed982f..dc5da6b7 100644
--- a/charms/placement-k8s/tests/unit/test_placement_charm.py
+++ b/charms/placement-k8s/tests/unit/test_placement_charm.py
@@ -18,9 +18,8 @@
 
 import textwrap
 
-import ops_sunbeam.test_utils as test_utils
-
 import charm
+import ops_sunbeam.test_utils as test_utils
 
 
 class _PlacementOperatorCharm(charm.PlacementOperatorCharm):
@@ -115,6 +114,8 @@ class TestPlacementOperatorCharm(test_utils.CharmTestCase):
 
         [placement_database]
         connection = mysql+pymysql://foo:hardpassword@10.0.0.10/placement_api
+
+
         [keystone_authtoken]
         auth_url = http://keystone.internal:5000
         interface = internal
diff --git a/charms/placement-k8s/tox.ini b/charms/placement-k8s/tox.ini
deleted file mode 100644
index 0ab1edc3..00000000
--- a/charms/placement-k8s/tox.ini
+++ /dev/null
@@ -1,161 +0,0 @@
-# Source charm: ./tox.ini
-# This file is managed centrally by release-tools and should not be modified
-# within individual charm repos.  See the 'global' dir contents for available
-# choices of tox.ini for OpenStack Charms:
-#     https://github.com/openstack-charmers/release-tools
-
-[tox]
-skipsdist = True
-envlist = pep8,py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
-passenv =
-  PYTHONPATH
-install_command =
-  pip install {opts} {packages}
-commands = stestr run --slowest {posargs}
-allowlist_externals =
-  git
-  charmcraft
-  {toxinidir}/fetch-libs.sh
-  {toxinidir}/rename.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:build]
-basepython = python3
-deps =
-commands =
-  charmcraft -v pack
-  {toxinidir}/rename.sh
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    functest-run-suite --help
-
-[testenv:func]
-basepython = python3
-commands =
-    functest-run-suite --keep-model
-
-[testenv:func-smoke]
-basepython = python3
-setenv =
-    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
-    TEST_MAX_RESOLVE_COUNT = 5
-commands =
-    functest-run-suite --keep-model --smoke
-
-[testenv:func-dev]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --dev
-
-[testenv:func-target]
-basepython = python3
-commands =
-    functest-run-suite --keep-model --bundle {posargs}
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    tests/*
-    src/templates/*
-    lib/*
-
-[flake8]
-ignore=E226,W504
diff --git a/common.sh b/common.sh
new file mode 100644
index 00000000..eb5e5d76
--- /dev/null
+++ b/common.sh
@@ -0,0 +1,418 @@
+#!/bin/bash
+
+# All libraries required by sunbeam charms are centrally
+# maintained in libs folder. The libraries created by
+# sunbeam charms are placed in libs/internal and the
+# libraries provided by external charms are maintained
+# in libs/external.
+# All generic template parts are maintained in
+# templates/parts folder.
+#
+# This script provides functions for each sunbeam charms
+# all the common files that should be copied to charm
+# for building the charm and function testing.
+
+
+NULL_ARRAY=()
+
+# Internal libs for component. If libs are repeated, reuse the existing component
+INTERNAL_CEILOMETER_LIBS=(
+	"keystone_k8s"
+	"ceilometer_k8s"
+	"gnocchi_k8s"
+)
+
+INTERNAL_CINDER_LIBS=(
+        "keystone_k8s"
+        "cinder_k8s"
+)
+
+INTERNAL_CINDER_CEPH_LIBS=(
+        "keystone_k8s"
+        "cinder_k8s"
+        "cinder_ceph_k8s"
+)
+
+INTERNAL_DESIGNATE_LIBS=(
+        "keystone_k8s"
+        "designate_bind_k8s"
+)
+
+INTERNAL_DESIGNATE_BIND_LIBS=(
+        "designate_bind_k8s"
+)
+
+INTERNAL_GNOCCHI_LIBS=(
+        "keystone_k8s"
+        "gnocchi_k8s"
+)
+
+INTERNAL_KEYSTONE_LIBS=(
+        "keystone_k8s"
+)
+
+INTERNAL_NEUTRON_LIBS=(
+        "keystone_k8s"
+        "ovn_central_k8s"
+)
+
+INTERNAL_NOVA_LIBS=(
+	"keystone_k8s"
+	"sunbeam_nova_compute_operator"
+)
+
+INTERNAL_OPENSTACK_HYPERVISOR_LIBS=(
+	"keystone_k8s"
+	"ovn_central_k8s"
+	"cinder_ceph_k8s"
+	"ceilometer_k8s"
+)
+
+INTERNAL_OVN_CENTRAL_LIBS=(
+        "ovn_central_k8s"
+)
+
+# External libs for component. If libs are repeated, reuse the existing component
+EXTERNAL_AODH_LIBS=(
+	"data_platform_libs"
+	"rabbitmq_k8s"
+	"traefik_k8s"
+)
+
+EXTERNAL_BARBICAN_LIBS=(
+        "data_platform_libs"
+        "rabbitmq_k8s"
+        "traefik_k8s"
+	"vault_k8s"
+)
+
+EXTERNAL_CEILOMETER_LIBS=(
+        "rabbitmq_k8s"
+)
+
+EXTERNAL_DESIGNATE_BIND_LIBS=(
+	"observability_libs"
+)
+
+EXTERNAL_HEAT_LIBS=(
+        "data_platform_libs"
+        "rabbitmq_k8s"
+        "traefik_route_k8s"
+)
+
+EXTERNAL_NEUTRON_LIBS=(
+        "data_platform_libs"
+        "rabbitmq_k8s"
+        "traefik_k8s"
+	"tls_certificates_interface"
+)
+
+EXTERNAL_OCTAVIA_LIBS=(
+        "data_platform_libs"
+        "traefik_k8s"
+        "tls_certificates_interface"
+)
+
+EXTERNAL_OPENSTACK_EXPORTER_LIBS=(
+        "grafana_k8s"
+        "prometheus_k8s"
+        "tls_certificates_interface"
+)
+
+EXTERNAL_OPENSTACK_HYPERVISOR_LIBS=(
+	"data_platform_libs"
+        "grafana_agent"
+	"observability_libs"
+	"operator_libs_linux"
+	"rabbitmq_k8s"
+        "traefik_k8s"
+        "tls_certificates_interface"
+)
+
+EXTERNAL_OVN_CENTRAL_LIBS=(
+        "tls_certificates_interface"
+)
+
+EXTERNAL_OVN_RELAY_LIBS=(
+        "tls_certificates_interface"
+	"observability_libs"
+)
+
+# Config template parts for each component.
+CONFIG_TEMPLATES_AODH=(
+	"section-database"
+	"database-connection"
+	"section-identity"
+	"identity-data"
+	"section-oslo-messaging-rabbit"
+	"section-service-credentials"
+)
+
+CONFIG_TEMPLATES_BARBICAN=(
+        "section-identity"
+	"identity-data"
+        "section-oslo-messaging-rabbit"
+        "section-service-user"
+)
+
+CONFIG_TEMPLATES_CEILOMETER=(
+	"identity-data-id-creds"
+	"section-oslo-messaging-rabbit"
+	"section-service-credentials-from-identity-service"
+	"section-service-user-from-identity-credentials"
+)
+
+CONFIG_TEMPLATES_CINDER=(
+        "section-database"
+	"database-connection"
+        "section-identity"
+	"identity-data"
+        "section-oslo-messaging-rabbit"
+        "section-service-user"
+)
+
+CONFIG_TEMPLATES_CINDER_CEPH=(
+	"section-oslo-messaging-rabbit"
+	"section-oslo-notifications"
+)
+
+CONFIG_TEMPLATES_DESIGNATE=(
+	"database-connection"
+	"section-identity"
+	"identity-data"
+	"section-oslo-messaging-rabbit"
+        "section-service-user"
+)
+
+CONFIG_TEMPLATES_GLANCE=(
+	"section-database"
+	"database-connection"
+	"section-identity"
+        "identity-data"
+	"section-oslo-messaging-rabbit"
+        "section-oslo-notifications"
+	"section-service-user"
+)
+
+CONFIG_TEMPLATES_GNOCCHI=(
+        "database-connection"
+        "section-identity"
+        "identity-data"
+)
+
+CONFIG_TEMPLATES_HEAT=(
+        "section-database"
+        "database-connection"
+        "section-identity"
+        "identity-data"
+        "section-oslo-messaging-rabbit"
+)
+
+CONFIG_TEMPLATES_KEYSTONE=(
+	"section-database"
+	"database-connection"
+	"section-federation"
+	"section-middleware"
+	"section-oslo-cache"
+	"section-oslo-messaging-rabbit"
+	"section-oslo-middleware"
+	"section-oslo-notifications"
+	"section-signing"
+)
+
+CONFIG_TEMPLATES_MAGNUM=(
+	"section-identity"
+        "identity-data"
+	"section-oslo-messaging-rabbit"
+	"section-service-user"
+	"section-trust"
+)
+
+CONFIG_TEMPLATES_NEUTRON=(
+        "section-database"
+        "database-connection"
+        "section-identity"
+        "identity-data"
+        "section-oslo-messaging-rabbit"
+        "section-service-user"
+)
+
+CONFIG_TEMPLATES_NOVA=${CONFIG_TEMPLATES_NEUTRON[@]}
+
+CONFIG_TEMPLATES_OCTAVIA=(
+        "section-database"
+        "database-connection"
+        "section-identity"
+        "identity-data"
+)
+
+CONFIG_TEMPLATES_PLACEMENT=(
+        "database-connection"
+        "section-identity"
+        "identity-data"
+        "section-service-user"
+)
+
+declare -A INTERNAL_LIBS=(
+	[aodh-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[barbican-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[ceilometer-k8s]=${INTERNAL_CEILOMETER_LIBS[@]}
+	[cinder-k8s]=${INTERNAL_CINDER_LIBS[@]}
+        [cinder-ceph-k8s]=${INTERNAL_CINDER_CEPH_LIBS[@]}
+	[designate-k8s]=${INTERNAL_DESIGNATE_LIBS[@]}
+	[designate-bind-k8s]=${INTERNAL_DESIGNATE_BIND_LIBS[@]}
+	[glance-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[gnocchi-k8s]=${INTERNAL_GNOCCHI_LIBS[@]}
+	[heat-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[horizon-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+        [keystone-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[keystone-ldap-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[magnum-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[neutron-k8s]=${INTERNAL_NEUTRON_LIBS[@]}
+	[nova-k8s]=${INTERNAL_NOVA_LIBS[@]}
+	[octavia-k8s]=${INTERNAL_NEUTRON_LIBS[@]}
+	[openstack-exporter-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+	[openstack-hypervisor]=${INTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]}
+	[ovn-central-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]}
+        [ovn-relay-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]}
+	[placement-k8s]=${INTERNAL_KEYSTONE_LIBS[@]}
+)
+
+declare -A EXTERNAL_LIBS=(
+        [aodh-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [barbican-k8s]=${EXTERNAL_BARBICAN_LIBS[@]}
+        [ceilometer-k8s]=${EXTERNAL_CEILOMETER_LIBS[@]}
+        [cinder-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [cinder-ceph-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [designate-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [designate-bind-k8s]=${EXTERNAL_DESIGNATE_BIND_LIBS[@]}
+        [glance-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [gnocchi-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [heat-k8s]=${EXTERNAL_HEAT_LIBS[@]}
+        [horizon-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [keystone-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [keystone-ldap-k8s]=${NULL_ARRAY[@]}
+        [magnum-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]}
+        [nova-k8s]=${EXTERNAL_AODH_LIBS[@]}
+        [octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]}
+        [openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]}
+        [openstack-hypervisor]=${EXTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]}
+	[ovn-central-k8s]=${EXTERNAL_OVN_CENTRAL_LIBS[@]}
+        [ovn-relay-k8s]=${EXTERNAL_OVN_RELAY_LIBS[@]}
+        [placement-k8s]=${EXTERNAL_AODH_LIBS[@]}
+)
+
+declare -A CONFIG_TEMPLATES=(
+        [aodh-k8s]=${CONFIG_TEMPLATES_AODH[@]}
+        [barbican-k8s]=${CONFIG_TEMPLATES_BARBICAN[@]}
+        [ceilometer-k8s]=${CONFIG_TEMPLATES_CEILOMETER[@]}
+        [cinder-k8s]=${CONFIG_TEMPLATES_CINDER[@]}
+        [cinder-ceph-k8s]=${CONFIG_TEMPLATES_CINDER_CEPH[@]}
+        [designate-k8s]=${CONFIG_TEMPLATES_DESIGNATE[@]}
+        [designate-bind-k8s]=${NULL_ARRAY[@]}
+        [glance-k8s]=${CONFIG_TEMPLATES_GLANCE[@]}
+	[gnocchi-k8s]=${CONFIG_TEMPLATES_GNOCCHI[@]}
+        [heat-k8s]=${CONFIG_TEMPLATES_HEAT[@]}
+        [horizon-k8s]=${NULL_ARRAY[@]}
+        [keystone-k8s]=${CONFIG_TEMPLATES_KEYSTONE[@]}
+        [keystone-ldap-k8s]=${NULL_ARRAY[@]}
+        [magnum-k8s]=${CONFIG_TEMPLATES_MAGNUM[@]}
+        [neutron-k8s]=${CONFIG_TEMPLATES_NEUTRON[@]}
+        [nova-k8s]=${CONFIG_TEMPLATES_NOVA[@]}
+        [octavia-k8s]=${CONFIG_TEMPLATES_OCTAVIA[@]}
+        [openstack-exporter-k8s]=${NULL_ARRAY[@]}
+        [openstack-hypervisor]=${NULL_ARRAY[@]}
+        [ovn-central-k8s]=${NULL_ARRAY[@]}
+        [ovn-relay-k8s]=${NULL_ARRAY[@]}
+        [placement-k8s]=${CONFIG_TEMPLATES_PLACEMENT[@]}
+)
+
+
+function copy_ops_sunbeam {
+	cp -rf ../../ops-sunbeam/ops_sunbeam lib/
+}
+
+function copy_internal_libs {
+	internal_libs_=${INTERNAL_LIBS[$1]}
+	echo "copy_internal_libs for $1:"
+	for lib in ${internal_libs_[@]}; do
+		echo "Copying $lib"
+                cp -rf ../../libs/internal/lib/charms/$lib lib/charms/
+        done
+}
+
+function copy_external_libs {
+	echo "copy_external_libs for $1:"
+	external_libs_=${EXTERNAL_LIBS[$1]}
+	for lib in ${external_libs_[@]}; do
+		echo "Copying $lib"
+                cp -rf ../../libs/external/lib/charms/$lib lib/charms/
+        done
+}
+
+function copy_config_templates {
+	echo "copy_config_templates for $1:"
+	config_templates_=${CONFIG_TEMPLATES[$1]}
+	for part in ${config_templates_[@]}; do
+		echo "Copying $part"
+                cp -rf ../../templates/parts/$part src/templates/parts/
+        done
+}
+
+function copy_juju_ignore {
+        cp ../../.jujuignore .
+}
+
+function copy_stestr_conf {
+	cp ../../.stestr.conf .
+}
+
+function remove_libs {
+	rm -rf lib
+}
+
+function remove_templates_parts_dir {
+	rm -rf src/templates/parts
+}
+
+function remove_juju_ignore {
+	rm .jujuignore
+}
+
+function remove_stestr_conf {
+	rm .stestr.conf
+}
+
+function push_common_files {
+	if [[ $# != 1 ]];
+	then
+		echo "push_common_files: Expected one argument"
+		exit 1
+	fi
+
+	pushd charms/$1
+
+        mkdir -p lib/charms
+        mkdir -p src/templates/parts
+
+        copy_ops_sunbeam
+	copy_internal_libs $1
+	copy_external_libs $1
+	copy_config_templates $1
+	copy_stestr_conf
+	copy_juju_ignore
+
+	popd
+}
+
+function pop_common_files {
+	pushd charms/$1
+
+	remove_libs
+        remove_templates_parts_dir
+	remove_stestr_conf
+        remove_juju_ignore
+
+	popd
+}
diff --git a/fetch_libs.sh b/fetch_libs.sh
new file mode 100755
index 00000000..8521bcc7
--- /dev/null
+++ b/fetch_libs.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+pushd libs/external
+
+echo "INFO: Fetching libs from charmhub."
+charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
+charmcraft fetch-lib charms.grafana_k8s.v0.grafana_auth
+charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
+charmcraft fetch-lib charms.operator_libs_linux.v2.snap
+charmcraft fetch-lib charms.prometheus_k8s.v0.prometheus_scrape
+charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
+charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
+charmcraft fetch-lib charms.traefik_k8s.v2.ingress
+charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route
+charmcraft fetch-lib charms.vault_k8s.v0.vault_kv
+
+popd
diff --git a/charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/libs/external/lib/charms/data_platform_libs/v0/database_requires.py
similarity index 100%
rename from charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py
rename to libs/external/lib/charms/data_platform_libs/v0/database_requires.py
diff --git a/charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py b/libs/external/lib/charms/grafana_agent/v0/cos_agent.py
similarity index 100%
rename from charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py
rename to libs/external/lib/charms/grafana_agent/v0/cos_agent.py
diff --git a/charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py b/libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py
similarity index 100%
rename from charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py
rename to libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py
diff --git a/charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py
similarity index 100%
rename from charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py
rename to libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py
diff --git a/charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py b/libs/external/lib/charms/operator_libs_linux/v2/snap.py
similarity index 100%
rename from charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py
rename to libs/external/lib/charms/operator_libs_linux/v2/snap.py
diff --git a/charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py b/libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py
similarity index 100%
rename from charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py
rename to libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py
diff --git a/charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
similarity index 100%
rename from charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
rename to libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py
diff --git a/charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py
similarity index 100%
rename from charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py
rename to libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py
diff --git a/charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py b/libs/external/lib/charms/traefik_k8s/v2/ingress.py
similarity index 100%
rename from charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py
rename to libs/external/lib/charms/traefik_k8s/v2/ingress.py
diff --git a/charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py b/libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py
similarity index 100%
rename from charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py
rename to libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py
diff --git a/charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py b/libs/external/lib/charms/vault_k8s/v0/vault_kv.py
similarity index 100%
rename from charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py
rename to libs/external/lib/charms/vault_k8s/v0/vault_kv.py
diff --git a/charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py
similarity index 100%
rename from charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py
rename to libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py
diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py
similarity index 100%
rename from charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py
rename to libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py
diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py
similarity index 100%
rename from charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py
rename to libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py
diff --git a/charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py
similarity index 100%
rename from charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py
rename to libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py
diff --git a/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py
similarity index 100%
rename from charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py
rename to libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py
diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py
similarity index 100%
rename from charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py
rename to libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py
similarity index 100%
rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py
rename to libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/libs/internal/lib/charms/keystone_k8s/v0/domain_config.py
similarity index 100%
rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py
rename to libs/internal/lib/charms/keystone_k8s/v0/domain_config.py
diff --git a/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py
similarity index 100%
rename from charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py
rename to libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py
diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py
similarity index 100%
rename from charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py
rename to libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_service.py
similarity index 100%
rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py
rename to libs/internal/lib/charms/keystone_k8s/v0/identity_service.py
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py
similarity index 100%
rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py
rename to libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py
diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v1/identity_service.py
similarity index 100%
rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py
rename to libs/internal/lib/charms/keystone_k8s/v1/identity_service.py
diff --git a/charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py
similarity index 100%
rename from charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py
rename to libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py
diff --git a/charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py b/libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py
similarity index 100%
rename from charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py
rename to libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py
diff --git a/ops-sunbeam/pyproject.toml b/ops-sunbeam/pyproject.toml
deleted file mode 100644
index 2896bc05..00000000
--- a/ops-sunbeam/pyproject.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-# Testing tools configuration
-[tool.coverage.run]
-branch = true
-
-[tool.coverage.report]
-show_missing = true
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-log_cli_level = "INFO"
-
-# Formatting tools configuration
-[tool.black]
-line-length = 79
-
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-force_grid_wrap = true
-
-# Linting tools configuration
-[tool.flake8]
-max-line-length = 79
-max-doc-length = 99
-max-complexity = 10
-exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
-select = ["E", "W", "F", "C", "N", "R", "D", "H"]
-# Ignore W503, E501 because using black creates errors with this
-# Ignore D107 Missing docstring in __init__
-ignore = ["W503", "E501", "D107", "E402"]
-per-file-ignores = []
-docstring-convention = "google"
-# Check for properly formatted copyright header in each file
-copyright-check = "True"
-copyright-author = "Canonical Ltd."
-copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/ops-sunbeam/test-requirements.txt b/ops-sunbeam/test-requirements.txt
deleted file mode 100644
index b196466f..00000000
--- a/ops-sunbeam/test-requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-coverage
-mock
-stestr
-requests
-pytest
-ops-scenario>=4.0
diff --git a/ops-sunbeam/tox.ini b/ops-sunbeam/tox.ini
deleted file mode 100644
index 2cdf6310..00000000
--- a/ops-sunbeam/tox.ini
+++ /dev/null
@@ -1,135 +0,0 @@
-# Operator charm helper: tox.ini
-
-[tox]
-skipsdist = True
-envlist = lint, py3
-sitepackages = False
-skip_missing_interpreters = False
-minversion = 3.18.0
-
-[vars]
-src_path = {toxinidir}/ops_sunbeam
-tst_path = {toxinidir}/tests/unit_tests/
-scenario_tst_path = {toxinidir}/tests/scenario_tests/
-tst_lib_path = {toxinidir}/tests/lib/
-pyproject_toml = {toxinidir}/pyproject.toml
-cookie_cutter_path = {toxinidir}/shared_code/sunbeam_charm/\{\{cookiecutter.service_name\}\}
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-install_command =
-  pip install {opts} {packages}
-commands =
-    stestr run --slowest {posargs}
-    pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO
-allowlist_externals =
-  git
-  charmcraft
-  fetch-libs.sh
-deps =
-  -r{toxinidir}/test-requirements.txt
-
-[testenv:fmt]
-description = Apply coding style standards to code
-deps =
-    black
-    isort
-commands =
-    isort {[vars]all_path} --skip-glob {[vars]tst_lib_path} --skip {toxinidir}/.tox
-    black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]tst_lib_path}
-
-[testenv:fetch]
-basepython = python3
-deps =
-commands =
-  {toxinidir}/fetch-libs.sh
-
-[testenv:cookie]
-basepython = python3
-deps = -r{toxinidir}/cookie-requirements.txt
-commands = /bin/true
-
-[testenv:py3]
-basepython = python3
-deps =
-  {[testenv]deps}
-  -r{toxinidir}/requirements.txt
-
-[testenv:py38]
-basepython = python3.8
-deps = {[testenv:py3]deps}
-
-[testenv:py39]
-basepython = python3.9
-deps = {[testenv:py3]deps}
-
-[testenv:py310]
-basepython = python3.10
-deps = {[testenv:py3]deps}
-
-[testenv:py311]
-basepython = python3.11
-deps = {[testenv:py3]deps}
-
-[testenv:pep8]
-description = Alias for lint
-deps = {[testenv:lint]deps}
-commands = {[testenv:lint]commands}
-
-[testenv:lint]
-description = Check code against coding style standards
-deps =
-    black
-    flake8<6
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    codespell {[vars]all_path} 
-    # pflake8 wrapper supports config from pyproject.toml
-    pflake8 --exclude {[vars]tst_lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
-    isort --check-only --diff {[vars]all_path} --skip-glob {[vars]tst_lib_path}
-    black  --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]tst_lib_path}
-
-[testenv:cover]
-basepython = python3
-deps = {[testenv:py3]deps}
-setenv =
-    PYTHON=coverage run
-commands =
-    coverage erase
-    stestr run --slowest {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-    coverage report
-
-[testenv:scenario]
-description = Scenario tests
-deps =
-    -r{toxinidir}/requirements.txt
-    -r{toxinidir}/test-requirements.txt
-commands =
-    pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-    .
-omit =
-    .tox/*
-    unit_tests/*
-
-[testenv:venv]
-basepython = python3
-commands = {posargs}
-
-[flake8]
-ignore = E226,E402,ANN101,ANN003,W504
diff --git a/playbooks/charm/build.yaml b/playbooks/charm/build.yaml
new file mode 100644
index 00000000..e794a3ad
--- /dev/null
+++ b/playbooks/charm/build.yaml
@@ -0,0 +1,6 @@
+- hosts: all
+  roles:
+    - ensure-tox
+    - role: charm-build
+      vars:
+        charm_build_name: "{{ charm }}"
diff --git a/playbooks/charm/publish.yaml b/playbooks/charm/publish.yaml
new file mode 100644
index 00000000..547b9240
--- /dev/null
+++ b/playbooks/charm/publish.yaml
@@ -0,0 +1,3 @@
+- hosts: all
+  roles:
+    - charm-publish
diff --git a/playbooks/collect-run-data.yaml b/playbooks/collect-run-data.yaml
new file mode 100644
index 00000000..cf81022a
--- /dev/null
+++ b/playbooks/collect-run-data.yaml
@@ -0,0 +1,3 @@
+- hosts: all
+  roles:
+    - collect-run-data
diff --git a/playbooks/zaza-func-test.yaml b/playbooks/zaza-func-test.yaml
new file mode 100644
index 00000000..41e5c315
--- /dev/null
+++ b/playbooks/zaza-func-test.yaml
@@ -0,0 +1,6 @@
+- hosts: all
+  roles:
+    - ensure-tox
+    - use-docker-mirror
+    - microk8s-cloud
+    - zaza-func-test
diff --git a/charms/aodh-k8s/pyproject.toml b/pyproject.toml
similarity index 100%
rename from charms/aodh-k8s/pyproject.toml
rename to pyproject.toml
diff --git a/render_bundles.py b/render_bundles.py
new file mode 100644
index 00000000..4a10bddf
--- /dev/null
+++ b/render_bundles.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright 2023 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.
+
+
+"""Render all smoke bundles.
+
+Renders smoke bundles with context of locally built charms.
+Prepares the context with assumption the charm is locally
+built if corresponding *.charm exists in current folder.
+
+Assumption: All build charms will be in sunbeam-charms folder.
+"""
+
+import glob
+from pathlib import (
+    Path,
+)
+
+from jinja2 import (
+    Environment,
+    FileSystemLoader,
+)
+
+test_directories = [ dir_.name for dir_ in list(Path("tests").glob('*')) ]
+built_charms = glob.glob("*.charm")
+context = {
+    charm.rstrip(".charm").replace("-", "_"): True for charm in built_charms
+}
+print(f"Using context: {context}")
+
+for test_dir in test_directories:
+    bundle_dir = f"tests/{test_dir}"
+    template_loader = Environment(loader=FileSystemLoader(bundle_dir))
+    bundle_template = template_loader.get_template("smoke.yaml.j2")
+    smoke_file = Path(f"{bundle_dir}/bundles/smoke.yaml")
+    smoke_file.parent.mkdir(parents=True, exist_ok=True)
+    with smoke_file.open("w", encoding="utf-8") as content:
+        content.write(bundle_template.render(context))
+        print(f"Rendered smoke bundle: {smoke_file}")
+    with smoke_file.open("r", encoding="utf-8") as content:
+        print(content.read())
diff --git a/roles/charm-build/tasks/main.yaml b/roles/charm-build/tasks/main.yaml
new file mode 100644
index 00000000..f4e3bd7e
--- /dev/null
+++ b/roles/charm-build/tasks/main.yaml
@@ -0,0 +1,69 @@
+- name: lxd apt packages are not present
+  apt:
+    name:
+      - lxd
+      - lxd-client
+    state: absent
+    purge: true
+  become: true
+
+- name: snapd is installed
+  apt:
+    name: snapd
+  become: true
+
+- name: lxd snap is installed
+  snap:
+    name: lxd
+    channel: latest/stable
+  become: true
+
+- name: lxd is initialised
+  command: lxd init --auto
+  become: true
+
+- name: current user is in lxd group
+  user:
+    name: "{{ ansible_user }}"
+    groups: lxd
+    append: true
+  become: true
+
+- name: reset ssh connection to apply permissions from new group
+  meta: reset_connection
+
+- name: charmcraft is installed
+  snap:
+    name: charmcraft
+    channel: "{{ charmcraft_channel | default('latest/stable') }}"
+    classic: true
+  become: true
+
+- name: charm is packed
+  command:
+    cmd: "{{ tox_executable }} -e build -- {{ charm_build_name }}"
+    chdir: "{{ zuul.project.src_dir }}"
+  register: res
+  retries: 3
+  delay: 30
+  until: >
+    "Charm packed ok" in res.stdout
+  failed_when: '"Failed instance creation" in res.stdout'
+
+- name: built charm is available in the zuul log root for auto artifact upload
+  fetch:
+    src: "{{ zuul.project.src_dir }}/charms/{{ charm_build_name }}/{{ charm_build_name }}.charm"
+    dest: "{{ zuul.executor.log_root }}/"
+    flat: true
+  become: true
+
+- name: Upload artifacts
+  zuul_return:
+    data:
+      zuul:
+        artifacts:
+          - name: charm
+            url: "{{ charm_build_name }}.charm"
+            metadata:
+              type: charm
+              name: "{{ charm_build_name }}"
diff --git a/roles/charm-publish/defaults/main.yaml b/roles/charm-publish/defaults/main.yaml
new file mode 100644
index 00000000..a9306832
--- /dev/null
+++ b/roles/charm-publish/defaults/main.yaml
@@ -0,0 +1,23 @@
+publish_channels:
+  keystone-k8s: latest/edge
+  glance-k8s: latest/edge
+  nova-k8s: latest/edge
+  placement-k8s: latest/edge
+  neutron-k8s: latest/edge
+  ovn-central-k8s: latest/edge
+  ovn-relay-k8s: latest/edge
+  cinder-k8s: latest/edge
+  cinder-ceph-k8s: latest/edge
+  horizon-k8s: latest/edge
+  heat-k8s: latest/edge
+  octavia-k8s: latest/edge
+  aodh-k8s: latest/edge
+  ceilometer-k8s: latest/edge
+  gnocchi-k8s: latest/edge
+  barbican-k8s: latest/edge
+  designate-k8s: latest/edge
+  designate-bind-k8s: latest/edge
+  magnum-k8s: latest/edge
+  keystone-ldap-k8s: latest/edge
+  openstack-exporter-k8s: latest/edge
+  openstack-hypervisor: latest/edge
diff --git a/roles/charm-publish/tasks/main.yaml b/roles/charm-publish/tasks/main.yaml
new file mode 100644
index 00000000..7a851492
--- /dev/null
+++ b/roles/charm-publish/tasks/main.yaml
@@ -0,0 +1,53 @@
+- name: Get all job names from gate pipeline
+  uri:
+    url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}"
+  register: build_output
+  vars:
+    download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}"
+    download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=gate"
+
+- name: Get relevant charm build jobs
+  set_fact:
+    relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list }}"
+
+- name: Print relevant build jobs
+  debug:
+    msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}"
+
+- name: built charm is present locally (artifact from gate pipeline)
+  include_role:
+    name: download-artifact
+  vars:
+    download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}"
+    download_artifact_type: charm
+    download_artifact_pipeline: gate
+    download_artifact_job: "{{ item }}"
+    download_artifact_directory: "{{ zuul.project.src_dir }}"
+  with_items: "{{ relevant_charm_build_jobs }}"
+
+- name: Get all downloaded charm names
+  args:
+    chdir: "{{ zuul.project.src_dir }}"
+    executable: /bin/bash
+  shell: |
+    ls *.charm | cut -d"." -f 1
+  register: built_charms
+
+- name: Prepare charm channel dict for downloaded charms
+  set_fact:
+    charm_channels: "{{ charm_channels | default({}) | combine({item.key: item.value}) }}"
+  loop: "{{ lookup('ansible.builtin.dict', publish_channels) }}"
+  when: "{{ item.key in built_charms.stdout_lines }}"
+
+- name: Print charm channel dict
+  debug:
+    msg: "Charms to be published: {{ charm_channels }}"
+
+- name: Publish charms in a loop
+  include_tasks: "publish.yaml"
+  vars:
+    charm_build_name: "{{ channel.key }}"
+    publish_channel: "{{ channel.value }}"
+  loop: "{{ charm_channels|dict2items }}"
+  loop_control:
+    loop_var: channel
diff --git a/roles/charm-publish/tasks/publish.yaml b/roles/charm-publish/tasks/publish.yaml
new file mode 100644
index 00000000..7d85d799
--- /dev/null
+++ b/roles/charm-publish/tasks/publish.yaml
@@ -0,0 +1,59 @@
+- name: Publish charms to charmhub
+  when: publish_charm
+  environment:
+    CHARMCRAFT_AUTH: "{{ charmhub_token.value }}"
+  block:
+    - name: Install docker
+      include_role:
+        name: ensure-docker
+
+    - name: Upload oci-image to charmhub
+      register: upload_oci_image_output
+      vars:
+        metadata: "{{ lookup('file', zuul.executor.work_root+'/'+zuul.project.src_dir+'/charms/'+charm_build_name+'/metadata.yaml') | from_yaml }}"
+      args:
+        executable: /bin/bash
+      shell: |
+        set -x
+        image={{ item.value['upstream-source'] }}
+        # Remove docker.io/ in the OCI image so that docker pulls image
+        # from mirror if configured.
+        image=${image#"docker.io/"}
+        docker pull $image
+        digest=`docker inspect --format {% raw %}'{{ index .RepoDigests 0 }}' {% endraw %} $image`
+        charmcraft upload-resource {{ charm_build_name }} {{ item.key }} --image $digest
+      retries: 3
+      until: >
+        ("Revision" in upload_oci_image_output.stdout)
+      loop: "{{ lookup('ansible.builtin.dict', metadata.resources|default({}), wantlist=True) }}"
+      when: "item.value.type == 'oci-image'"
+
+    - name: Extract Resource revisions
+      set_fact:
+        resource_revision_flags: "{{ resource_revision_flags | default('') + ' --resource ' + item.item.key + ':' + (item.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first) }}"
+      with_items: "{{ upload_oci_image_output.results }}"
+
+    - name: Upload charm to charmhub
+      register: upload_charm_output
+      args:
+        chdir: "{{ zuul.project.src_dir }}"
+      # TODO: The below command can error out with a message that says
+      # upload with that digest already exists. This case need to be handled.
+      # More details https://github.com/canonical/charmcraft/issues/826
+      command:
+        charmcraft upload -v --name {{ charm_build_name }} {{ charm_build_name }}.charm
+      retries: 3
+      until: >
+        ("Revision" in upload_charm_output.stdout)
+
+    - name: Extract Charm revision
+      set_fact:
+        charm_revision: "{{ upload_charm_output.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first }}"
+
+    - name: Release charm
+      register: release_charm_output
+      command:
+        charmcraft release {{ charm_build_name }} --revision {{ charm_revision }} --channel {{ publish_channel }} {{ resource_revision_flags | default("") }}
+      retries: 3
+      until: >
+        ("Revision" in release_charm_output.stdout)
diff --git a/roles/collect-run-data/tasks/main.yaml b/roles/collect-run-data/tasks/main.yaml
new file mode 100644
index 00000000..81123bc5
--- /dev/null
+++ b/roles/collect-run-data/tasks/main.yaml
@@ -0,0 +1,95 @@
+- name: test runner packages are installed
+  apt:
+    name:
+      - jq
+  become: true
+- name: Create destination for logs
+  file:
+    path: "{{ zuul.project.src_dir }}/log"
+    state: directory
+    mode: 0755
+- name: collect microk8s inspection report
+  args:
+    executable: /bin/bash
+  shell: |
+    cp /var/snap/microk8s/current/inspection-report-*.tar.gz "{{ zuul.project.src_dir }}/log/"
+  failed_when: false
+- name: debug logs replay
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')"
+    juju switch $MODEL
+    juju debug-log --replay > {{ zuul.project.src_dir }}/log/debug-hooks.txt
+    exit 0
+- name: debug describe pods
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')"
+    microk8s.kubectl describe -n $MODEL pods > {{ zuul.project.src_dir }}/log/describe-pods.txt
+    CONTROLLER_MODEL="$(microk8s.kubectl get ns | grep controller | awk '{print $1}')"
+    microk8s.kubectl describe -n $CONTROLLER_MODEL pods > {{ zuul.project.src_dir }}/log/describe-controller-pods.txt
+    exit 0
+- name: juju status
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    for model in $(juju models | grep zaza- | awk '{gsub(/\*?/,""); print $1}'); do
+      juju status -m $model > {{ zuul.project.src_dir }}/log/juju-status.$model.txt
+      juju status -m $model --format=yaml > {{ zuul.project.src_dir }}/log/juju-status.$model.yaml
+    done
+- name: Collect var logs
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')
+    UNITS=$(juju status --format oneline | awk '{print $2}' | sed -e 's!:!!' | grep -Ev '^$' | paste -s -d' ')
+    for UNIT_NAME in $UNITS; do
+        POD_NAME=$(echo $UNIT_NAME | sed -e 's!/!-!')
+        CONTAINERS=$(microk8s.kubectl get pods -n $MODEL_NAME $POD_NAME -o jsonpath='{.spec.containers[*].name}' | sed -e 's/charm //')
+        for CONTAINER in $CONTAINERS; do
+            juju ssh --container $CONTAINER -m $MODEL_NAME $UNIT_NAME "tar zcf /tmp/logs.tgz /var/log/"
+            juju scp --container $CONTAINER -m $MODEL_NAME $UNIT_NAME:/tmp/logs.tgz {{ zuul.project.src_dir }}/log/$POD_NAME-$CONTAINER.tgz
+        done
+    done
+- name: Collect pods logs
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    LOG_FOLDER={{ zuul.project.src_dir }}/log/pods/
+    MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')
+    mkdir -p $LOG_FOLDER
+    for pod in $(microk8s.kubectl get pods -n $MODEL_NAME -o=jsonpath='{.items[*].metadata.name}');
+    do
+      echo Collecting logs: $pod
+      microk8s.kubectl logs --ignore-errors -n $MODEL_NAME --all-containers $pod > $LOG_FOLDER/$pod.log
+    done
+- name: Collect units' info
+  args:
+    executable: /bin/bash
+  shell: |
+    set -o pipefail
+    set -x
+    LOG_FOLDER={{ zuul.project.src_dir }}/log/unit-info/
+    MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')
+    mkdir -p $LOG_FOLDER
+    for unit in $(juju status --format json | jq -r '[.applications[].units | keys[0]] | join("\n")');
+    do
+      echo Collecting unit info: $unit
+      unit_name=$(echo $unit | tr / -)
+      juju show-unit --output="$LOG_FOLDER/$unit_name.yaml" $unit
+    done
+- name: fetch juju logs
+  synchronize:
+    dest: "{{ zuul.executor.log_root }}"
+    mode: pull
+    src: "{{ zuul.project.src_dir }}/log"
+    verify_host: true
+    owner: false
+    group: false
diff --git a/roles/microk8s-cloud/tasks/main.yaml b/roles/microk8s-cloud/tasks/main.yaml
new file mode 100644
index 00000000..60b29b36
--- /dev/null
+++ b/roles/microk8s-cloud/tasks/main.yaml
@@ -0,0 +1,157 @@
+- name: snapd is installed
+  apt:
+    name: snapd
+  become: true
+
+- name: set microk8s related variables
+  set_fact:
+    microk8s_group: "{{ 'microk8s' if microk8s_classic_mode | default(true) else 'snap_microk8s' }}"
+    microk8s_command_escalation: "{{ false if microk8s_classic_mode | default(true) else true }}"
+
+- name: Disable ipv6
+  become: true
+  sysctl:
+    name: "net.ipv6.conf.all.disable_ipv6"
+    value: "1"
+    state: "present"
+    reload: "yes"
+
+- name: microk8s is installed
+  snap:
+    name: microk8s
+    classic: "{{ microk8s_classic_mode | default(true) }}"
+    channel: "{{ microk8s_channel | default('latest/stable') }}"
+  become: true
+
+- name: current user is in microk8s group
+  user:
+    name: "{{ ansible_user }}"
+    groups: "{{ microk8s_group }}"
+    append: true
+  become: true
+
+- name: reset ssh connection to apply permissions from new group
+  meta: reset_connection
+
+- name: microk8s status
+  block:
+    - name: microk8s status
+      command:
+        cmd: microk8s status --wait-ready --timeout 300
+  rescue:
+    - name: microk8s inspect
+      command:
+        cmd: microk8s inspect
+      become: "{{ microk8s_command_escalation }}"
+    - name: microk8s status
+      command:
+        # second chance to get status
+        cmd: microk8s status
+
+- name: Create docker.io certs dir
+  when:
+    - docker_mirror is defined
+  file:
+    path: /var/snap/microk8s/current/args/certs.d/docker.io
+    state: directory
+    owner: root
+    group: "{{ microk8s_group }}"
+    mode: '0770'
+
+- name: Render microk8s registry mirror template
+  when:
+    - docker_mirror is defined
+  template:
+    src: hosts.j2
+    dest: /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml
+    group: "{{ microk8s_group }}"
+  vars:
+    mirror_location: "{{ docker_mirror }}"
+    server: https://docker.io
+
+- name: Check docker.io hosts.toml
+  when:
+    - docker_mirror is defined
+  command:
+    cmd: cat /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml
+
+- name: microk8s is started
+  command:
+    cmd: microk8s start
+  become: "{{ microk8s_command_escalation }}"
+
+- name: microk8s is running and ready
+  command:
+    cmd: microk8s status --wait-ready
+  register: res
+  failed_when: '"is running" not in res.stdout'
+
+- name: microk8s dns addon is enabled
+  command:
+    cmd: microk8s enable dns
+  register: res
+  changed_when: '"already enabled" not in res.stdout'
+  become: "{{ microk8s_command_escalation }}"
+
+- name: microk8s hostpath storage addon is enabled
+  command:
+    cmd: microk8s enable hostpath-storage
+  register: res
+  changed_when: '"already enabled" not in res.stdout'
+  become: "{{ microk8s_command_escalation }}"
+
+- name: microk8s metallb addon is enabled
+  command:
+    # ip range is an arbitrary choice; may need to be changed later
+    cmd: microk8s enable metallb:10.170.0.1-10.170.0.100
+  register: res
+  changed_when: '"already enabled" not in res.stdout'
+  become: "{{ microk8s_command_escalation }}"
+
+- name: microk8s addons are ready
+  command:
+    cmd: microk8s status --format short
+  register: res
+  retries: 18
+  delay: 10  # 18 * 10 = 3 minutes
+  until: >
+    "core/dns: enabled" in res.stdout and
+    "core/hostpath-storage: enabled" in res.stdout and
+    "core/metallb: enabled" in res.stdout
+  changed_when: res.attempts > 1
+
+- name: juju is installed
+  snap:
+    name: juju
+    classic: "{{ juju_classic_mode | default(true) }}"
+    channel: "{{ juju_channel | default('latest/stable') }}"
+  become: true
+
+- name: Ensure ~/.local/share directory exist
+  file:
+    path: ~/.local/share
+    state: directory
+
+- name: juju is bootstrapped on microk8s
+  command:
+    cmd: juju bootstrap --config bootstrap-timeout=600 microk8s microk8s
+  register: res
+  retries: 3
+  delay: 10
+  until: >
+    "Bootstrap complete" in res.stderr or
+    "already exists" in res.stderr
+  failed_when: '"ERROR" in res.stderr and "already exists" not in res.stderr'
+
+- name: current juju controller is microk8s
+  command:
+    cmd: juju switch microk8s
+  register: res
+  changed_when: '"no change" not in res.stderr'
+
+- name: Collect snap versions
+  command: snap list
+  register: snap_out
+
+- name: Show snap versions
+  debug: msg="{{ snap_out.stdout }}"
diff --git a/roles/microk8s-cloud/templates/hosts.j2 b/roles/microk8s-cloud/templates/hosts.j2
new file mode 100644
index 00000000..a86649af
--- /dev/null
+++ b/roles/microk8s-cloud/templates/hosts.j2
@@ -0,0 +1,4 @@
+server = "{{ server }}"
+
+[host."{{ mirror_location }}"]
+  capabilities = ["pull", "resolve"]
diff --git a/roles/zaza-func-test/tasks/main.yaml b/roles/zaza-func-test/tasks/main.yaml
new file mode 100644
index 00000000..053d87de
--- /dev/null
+++ b/roles/zaza-func-test/tasks/main.yaml
@@ -0,0 +1,31 @@
+- name: Get all job names
+  uri:
+    url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}"
+  register: build_output
+  vars:
+    download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}"
+    download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=check"
+
+- name: Get relevant charm build jobs
+  set_fact:
+    relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list | intersect(charm_jobs) }}"
+
+- name: Print relevant build jobs
+  debug:
+    msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}"
+
+- name: built charm is present locally (artifact from previous job)
+  include_role:
+    name: download-artifact
+  vars:
+    download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}"
+    download_artifact_type: charm
+    download_artifact_pipeline: check
+    download_artifact_job: "{{ item }}"
+    download_artifact_directory: "{{ zuul.project.src_dir }}"
+  with_items: "{{ relevant_charm_build_jobs }}"
+
+- name: run smoke tests
+  command:
+    cmd: "{{ tox_executable }} -e func -- --smoke --test-directory={{ test_dir }}"
+    chdir: "{{ zuul.project.src_dir }}"
diff --git a/run_tox.sh b/run_tox.sh
new file mode 100755
index 00000000..a82cf957
--- /dev/null
+++ b/run_tox.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+
+source common.sh
+
+if [[ $1 == "fmt" ]];
+then
+	src_path_array=$(ls -d -1 "charms/"**/src)
+        tst_path_array=$(ls -d -1 "charms/"**/tests)
+        lib_path_array=$(ls -d -1 "charms/"**/lib)
+
+        src_path="${src_path_array[*]}"
+        tst_path="${tst_path_array[*]}"
+        lib_path="${lib_path_array[*]}"
+
+	isort ${src_path} ${tst_path}
+	black --config pyproject.toml ${src_path} ${tst_path}
+elif [[ $1 == "pep8" ]];
+then
+	src_path_array=$(ls -d -1 "charms/"**/src)
+	tst_path_array=$(ls -d -1 "charms/"**/tests)
+	
+	src_path="${src_path_array[*]}"
+	tst_path="${tst_path_array[*]}"
+
+	codespell ${src_path} ${tst_path}
+        pflake8 --config pyproject.toml ${src_path} ${tst_path}
+	isort --check-only --diff ${src_path} ${tst_path}
+	black --config pyproject.toml --check --diff ${src_path} ${tst_path}
+elif [[ $1 =~ ^(py3|py310|py311)$ ]];
+then
+	# Run py3 on ops-sunbeam
+	pushd ops-sunbeam
+        stestr run --slowest || exit 1
+        popd
+
+	# Run py3 on all sunbeam charms
+	charms=($(ls charms))
+	for charm in ${charms[@]}; do
+		push_common_files $charm || exit 1
+		pushd charms/$charm
+		PYTHONPATH=./src:./lib stestr run --slowest || exit 1
+		popd
+		pop_common_files $charm || exit 1
+	done
+elif [[ $1 == "cover" ]];
+then
+        coverage erase
+
+	# Run coverage on ops-sunbeam
+	pushd ops-sunbeam
+	coverage erase
+        PYTHON="coverage run --omit .tox/*,tests/*" stestr run --slowest || exit 1
+	coverage combine
+        popd
+
+	# Run coverage on all sunbeam charms
+	charms=($(ls charms))
+        for charm in ${charms[@]}; do
+		push_common_files $charm || exit 1
+                pushd charms/$charm
+		coverage erase
+                PYTHONPATH=./src:./lib:../../ops-sunbeam PYTHON="coverage run --omit .tox/*,tests/*,src/templates/*" stestr run --slowest || exit 1
+		coverage combine
+                popd
+        done
+
+	# Prepare coverage report
+	coverage combine charms/*/.coverage ops-sunbeam/.coverage
+	coverage html -d cover
+	coverage xml -o cover/coverage.xml
+	coverage report
+
+	# Common files should be deleted after coverage combine
+	for charm in ${charms[@]}; do
+		pop_common_files $charm || exit 1
+	done
+elif [[ $1 == "build" ]];
+then
+	if [[ $# != 2 ]];
+	then
+		echo "Command format: tox -e build <charm>"
+		exit 1
+	fi
+
+	charm=$2
+	charms=($(ls charms))
+	if [[ ! ${charms[@]} =~ $charm ]];
+	then
+		echo "Argument should be one of ${charms[@]}";
+		exit 1
+	fi
+
+	push_common_files $charm || exit 1
+
+	pushd charms/$charm
+	charmcraft -v pack || exit 1
+	if [[ -e "${charm}.charm" ]];
+	then
+		echo "Removing bad downloaded charm maybe?"
+		rm "${charm}.charm"
+	fi
+	echo "Renaming charm ${charm}_*.charm to ${charm}.charm"
+	mv ${charm}_*.charm ${charm}.charm
+	popd
+
+	pop_common_files $charm || exit 1
+else
+	echo "tox argument should be one of pep8, py3, py310, py311, cover";
+        exit 1
+fi
diff --git a/charms/aodh-k8s/src/templates/parts/database-connection b/templates/parts/database-connection
similarity index 55%
rename from charms/aodh-k8s/src/templates/parts/database-connection
rename to templates/parts/database-connection
index 1fd70ce2..21c25eb6 100644
--- a/charms/aodh-k8s/src/templates/parts/database-connection
+++ b/templates/parts/database-connection
@@ -1,3 +1,5 @@
 {% if database.connection -%}
 connection = {{ database.connection }}
+{% else -%}
+connection = sqlite:////var/lib/openstack/openstack.db
 {% endif -%}
diff --git a/charms/aodh-k8s/src/templates/parts/identity-data b/templates/parts/identity-data
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/identity-data
rename to templates/parts/identity-data
diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds b/templates/parts/identity-data-id-creds
similarity index 100%
rename from charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds
rename to templates/parts/identity-data-id-creds
diff --git a/charms/magnum-k8s/src/templates/parts/section-certificates b/templates/parts/section-certificates
similarity index 100%
rename from charms/magnum-k8s/src/templates/parts/section-certificates
rename to templates/parts/section-certificates
diff --git a/charms/aodh-k8s/src/templates/parts/section-database b/templates/parts/section-database
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-database
rename to templates/parts/section-database
diff --git a/charms/aodh-k8s/src/templates/parts/section-federation b/templates/parts/section-federation
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-federation
rename to templates/parts/section-federation
diff --git a/charms/aodh-k8s/src/templates/parts/section-identity b/templates/parts/section-identity
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-identity
rename to templates/parts/section-identity
diff --git a/charms/aodh-k8s/src/templates/parts/section-middleware b/templates/parts/section-middleware
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-middleware
rename to templates/parts/section-middleware
diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-cache b/templates/parts/section-oslo-cache
similarity index 100%
rename from charms/keystone-k8s/src/templates/parts/section-oslo-cache
rename to templates/parts/section-oslo-cache
diff --git a/charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit b/templates/parts/section-oslo-messaging-rabbit
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit
rename to templates/parts/section-oslo-messaging-rabbit
diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-middleware b/templates/parts/section-oslo-middleware
similarity index 100%
rename from charms/keystone-k8s/src/templates/parts/section-oslo-middleware
rename to templates/parts/section-oslo-middleware
diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications b/templates/parts/section-oslo-notifications
similarity index 100%
rename from charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications
rename to templates/parts/section-oslo-notifications
diff --git a/charms/aodh-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-service-credentials
rename to templates/parts/section-service-credentials
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials-from-identity-service
similarity index 100%
rename from charms/ceilometer-k8s/src/templates/parts/section-service-credentials
rename to templates/parts/section-service-credentials-from-identity-service
diff --git a/charms/barbican-k8s/src/templates/parts/section-service-user b/templates/parts/section-service-user
similarity index 100%
rename from charms/barbican-k8s/src/templates/parts/section-service-user
rename to templates/parts/section-service-user
diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds b/templates/parts/section-service-user-from-identity-credentials
similarity index 100%
rename from charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds
rename to templates/parts/section-service-user-from-identity-credentials
diff --git a/charms/aodh-k8s/src/templates/parts/section-signing b/templates/parts/section-signing
similarity index 100%
rename from charms/aodh-k8s/src/templates/parts/section-signing
rename to templates/parts/section-signing
diff --git a/charms/magnum-k8s/src/templates/parts/section-trust b/templates/parts/section-trust
similarity index 100%
rename from charms/magnum-k8s/src/templates/parts/section-trust
rename to templates/parts/section-trust
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 00000000..4cdc9db0
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,17 @@
+mock
+stestr
+coverage
+
+tenacity                 # ops-sunbeam
+ops                      # all charms
+lightkube                # almost all charms
+pwgen                    # keystone-k8s
+python-keystoneclient    # keystone-k8s
+cryptography             # neutron-k8s
+jsonschema               # neutron-k8s
+pytest-interface-tester  # barbican-k8s
+requests                 # cinder-ceph-k8s
+netifaces                # cinder-ceph-k8s
+cosl                     # openstack-exporter
+git+https://github.com/juju/charm-helpers.git#egg=charmhelpers                               # cinder-ceph-k8s,glance-k8s,gnocchi-k8s
+git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client  # cinder-ceph-k8s
diff --git a/tests/caas/smoke.yaml.j2 b/tests/caas/smoke.yaml.j2
new file mode 100644
index 00000000..9a26899b
--- /dev/null
+++ b/tests/caas/smoke.yaml.j2
@@ -0,0 +1,190 @@
+bundle: kubernetes
+
+applications:
+  traefik:
+    charm: ch:traefik-k8s
+    channel: 1.0/candidate
+    scale: 1
+    trust: true
+    options:
+      kubernetes-service-annotations: metallb.universe.tf/address-pool=public
+  mysql:
+    charm: ch:mysql-k8s
+    channel: 8.0/stable
+    scale: 1
+    trust: false
+  vault:
+    charm: ch:vault-k8s
+    channel: latest/edge
+    scale: 1
+    trust: false
+  tls-operator:
+    charm: self-signed-certificates
+    channel: latest/beta
+    scale: 1
+    options:
+      ca-common-name: internal-ca
+  rabbitmq:
+    charm: ch:rabbitmq-k8s
+    channel: 3.12/edge
+    scale: 1
+    trust: true
+    options:
+      minimum-replicas: 1
+  ovn-central:
+    {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%}
+    charm: ../../../ovn-central-k8s.charm
+    {% else -%}
+    charm: ch:ovn-central-k8s
+    channel: 23.03/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09
+      ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09
+      ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09
+  keystone:
+    {% if keystone_k8s is defined and keystone_k8s is sameas true -%}
+    charm: ../../../keystone-k8s.charm
+    {% else -%}
+    charm: ch:keystone-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    options:
+      admin-role: admin
+    storage:
+      fernet-keys: 5M
+      credential-keys: 5M
+    resources:
+      keystone-image: ghcr.io/canonical/keystone:2023.2
+  glance:
+    {% if glance_k8s is defined and glance_k8s is sameas true -%}
+    charm: ../../../glance-k8s.charm
+    {% else -%}
+    charm: ch:glance-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    storage:
+      local-repository: 5G
+    resources:
+      glance-api-image: ghcr.io/canonical/glance-api:2023.2
+  heat:
+    {% if heat_k8s is defined and heat_k8s is sameas true -%}
+    charm: ../../../heat-k8s.charm
+    {% else -%}
+    charm: ch:heat-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2
+      heat-engine-image:  ghcr.io/canonical/heat-consolidated:2023.2
+  octavia:
+    {% if octavia_k8s is defined and octavia_k8s is sameas true -%}
+    charm: ../../../octavia-k8s.charm
+    {% else -%}
+    charm: ch:octavia-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      octavia-api-image: ghcr.io/canonical/octavia-consolidated:2023.2
+      octavia-driver-agent-image: ghcr.io/canonical/octavia-consolidated:2023.2
+      octavia-housekeeping-image: ghcr.io/canonical/octavia-consolidated:2023.2
+  barbican:
+    {% if barbican_k8s is defined and barbican_k8s is sameas true -%}
+    charm: ../../../barbican-k8s.charm
+    {% else -%}
+    charm: ch:barbican-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: false
+    resources:
+      barbican-api-image: ghcr.io/canonical/barbican-consolidated:2023.2
+      barbican-worker-image: ghcr.io/canonical/barbican-consolidated:2023.2
+  magnum:
+    {% if magnum_k8s is defined and magnum_k8s is sameas true -%}
+    charm: ../../../magnum-k8s.charm
+    {% else -%}
+    charm: ch:magnum-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: false
+    resources:
+      magnum-api-image: ghcr.io/canonical/magnum-consolidated:2023.2
+      magnum-conductor-image: ghcr.io/canonical/magnum-consolidated:2023.2
+
+relations:
+- - tls-operator:certificates
+  - ovn-central:certificates
+
+- - mysql:database
+  - keystone:database
+- - traefik:ingress
+  - keystone:ingress-public
+
+- - mysql:database
+  - glance:database
+- - keystone:identity-service
+  - glance:identity-service
+- - rabbitmq:amqp
+  - glance:amqp
+- - traefik:ingress
+  - glance:ingress-public
+
+- - mysql:database
+  - heat:database
+- - keystone:identity-service
+  - heat:identity-service
+- - keystone:identity-ops
+  - heat:identity-ops
+- - traefik:traefik-route
+  - heat:traefik-route-public
+- - rabbitmq:amqp
+  - heat:amqp
+
+- - mysql:database
+  - octavia:database
+- - keystone:identity-service
+  - octavia:identity-service
+- - keystone:identity-ops
+  - octavia:identity-ops
+- - traefik:ingress
+  - octavia:ingress-public
+- - tls-operator:certificates
+  - octavia:certificates
+- - octavia:ovsdb-cms
+  - ovn-central:ovsdb-cms
+
+- - mysql:database
+  - barbican:database
+- - rabbitmq:amqp
+  - barbican:amqp
+- - keystone:identity-service
+  - barbican:identity-service
+- - keystone:identity-ops
+  - barbican:identity-ops
+- - traefik:ingress
+  - barbican:ingress-public
+- - vault:vault-kv
+  - barbican:vault-kv
+
+- - mysql:database
+  - magnum:database
+- - rabbitmq:amqp
+  - magnum:amqp
+- - keystone:identity-service
+  - magnum:identity-service
+- - keystone:identity-ops
+  - magnum:identity-ops
+- - traefik:ingress
+  - magnum:ingress-public
diff --git a/tests/caas/tests.yaml b/tests/caas/tests.yaml
new file mode 100644
index 00000000..4c2cd46b
--- /dev/null
+++ b/tests/caas/tests.yaml
@@ -0,0 +1,56 @@
+gate_bundles:
+  - smoke
+smoke_bundles:
+  - smoke
+configure:
+  - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints
+  - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
+tests:
+  - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal
+tests_options:
+  trust:
+    - smoke
+  ignore_hard_deploy_errors:
+    - smoke
+
+  tempest:
+    default:
+      smoke: True
+
+target_deploy_status:
+  traefik:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  mysql:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  vault:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  tls-operator:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  rabbitmq:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  ovn-central:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  keystone:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  glance:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  heat:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  octavia:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  barbican:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  magnum:
+    workload-status: active
+    workload-status-message-regex: '^$'
diff --git a/tests/ceph/smoke.yaml.j2 b/tests/ceph/smoke.yaml.j2
new file mode 100644
index 00000000..82fff4cc
--- /dev/null
+++ b/tests/ceph/smoke.yaml.j2
@@ -0,0 +1,145 @@
+bundle: kubernetes
+
+applications:
+  traefik:
+    charm: ch:traefik-k8s
+    channel: 1.0/candidate
+    scale: 1
+    trust: true
+    options:
+      kubernetes-service-annotations: metallb.universe.tf/address-pool=public
+  mysql:
+    charm: ch:mysql-k8s
+    channel: 8.0/stable
+    scale: 1
+    trust: false
+  rabbitmq:
+    charm: ch:rabbitmq-k8s
+    channel: 3.12/edge
+    scale: 1
+    trust: true
+    options:
+      minimum-replicas: 1
+  keystone:
+    {% if keystone_k8s is defined and keystone_k8s is sameas true -%}
+    charm: ../../../keystone-k8s.charm
+    {% else -%}
+    charm: ch:keystone-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    options:
+      admin-role: admin
+    storage:
+      fernet-keys: 5M
+      credential-keys: 5M
+    resources:
+      keystone-image: ghcr.io/canonical/keystone:2023.2
+  cinder:
+    {% if cinder_k8s is defined and cinder_k8s is sameas true -%}
+    charm: ../../../cinder-k8s.charm
+    {% else -%}
+    charm: ch:cinder-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      cinder-api-image: ghcr.io/canonical/cinder-consolidated:2023.2
+      cinder-scheduler-image: ghcr.io/canonical/cinder-consolidated:2023.2
+  cinder-ceph:
+    {% if cinder_ceph_k8s is defined and cinder_ceph_k8s is sameas true -%}
+    charm: ../../../cinder-ceph-k8s.charm
+    {% else -%}
+    charm: ch:cinder-ceph-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      cinder-volume-image: ghcr.io/canonical/cinder-consolidated:2023.2
+  gnocchi:
+    {% if gnocchi_k8s is defined and gnocchi_k8s is sameas true -%}
+    charm: ../../../gnocchi-k8s.charm
+    {% else -%}
+    charm: ch:gnocchi-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      gnocchi-api-image: ghcr.io/canonical/gnocchi-consolidated:2023.1
+      gnocchi-metricd-image: ghcr.io/canonical/gnocchi-consolidated:2023.1
+  ceilometer:
+    {% if ceilometer_k8s is defined and ceilometer_k8s is sameas true -%}
+    charm: ../../../ceilometer-k8s.charm
+    {% else -%}
+    charm: ch:ceilometer-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      ceilometer-central-image: ghcr.io/canonical/ceilometer-consolidated:2023.2
+      ceilometer-notification-image: ghcr.io/canonical/ceilometer-consolidated:2023.2
+  aodh:
+    {% if aodh_k8s is defined and aodh_k8s is sameas true -%}
+    charm: ../../../aodh-k8s.charm
+    {% else -%}
+    charm: ch:aodh-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      aodh-api-image: ghcr.io/canonical/aodh-consolidated:2023.2
+      aodh-evaluator-image: ghcr.io/canonical/aodh-consolidated:2023.2
+      aodh-notifier-image: ghcr.io/canonical/aodh-consolidated:2023.2
+      aodh-listener-image: ghcr.io/canonical/aodh-consolidated:2023.2
+      aodh-expirer-image: ghcr.io/canonical/aodh-consolidated:2023.2
+
+relations:
+- - mysql:database
+  - keystone:database
+- - traefik:ingress
+  - keystone:ingress-public
+
+- - mysql:database
+  - cinder:database
+- - cinder:amqp
+  - rabbitmq:amqp
+- - keystone:identity-service
+  - cinder:identity-service
+- - traefik:ingress
+  - cinder:ingress-public
+
+- - cinder-ceph:database
+  - mysql:database
+- - cinder-ceph:amqp
+  - rabbitmq:amqp
+- - cinder:storage-backend
+  - cinder-ceph:storage-backend
+
+- - mysql:database
+  - gnocchi:database
+- - traefik:ingress
+  - gnocchi:ingress-public
+- - keystone:identity-service
+  - gnocchi:identity-service
+
+- - rabbitmq:amqp
+  - ceilometer:amqp
+- - keystone:identity-credentials
+  - ceilometer:identity-credentials
+- - gnocchi:gnocchi-service
+  - ceilometer:gnocchi-db
+
+- - mysql:database
+  - aodh:database
+- - rabbitmq:amqp
+  - aodh:amqp
+- - keystone:identity-service
+  - aodh:identity-service
+- - traefik:ingress
+  - aodh:ingress-public
diff --git a/tests/ceph/tests.yaml b/tests/ceph/tests.yaml
new file mode 100644
index 00000000..caec8bdc
--- /dev/null
+++ b/tests/ceph/tests.yaml
@@ -0,0 +1,43 @@
+gate_bundles:
+  - smoke
+smoke_bundles:
+  - smoke
+# There is no storage provider at the moment so cannot run tests.
+configure:
+  - zaza.charm_tests.noop.setup.basic_setup
+tests:
+  - zaza.charm_tests.noop.tests.NoopTest
+tests_options:
+  trust:
+    - smoke
+  ignore_hard_deploy_errors:
+    - smoke
+
+target_deploy_status:
+  traefik:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  mysql:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  rabbitmq:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  keystone:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  cinder:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  cinder-ceph:
+    workload-status: blocked
+    workload-status-message-regex: '^.*ceph.*$'
+  ceilometer:
+    workload-status: waiting
+    workload-status-message-regex: '^.*Not all relations are ready$'
+  aodh:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  gnocchi:
+    workload-status: blocked
+    workload-status-message-regex: '^.*ceph.*$'
diff --git a/tests/core/smoke.yaml.j2 b/tests/core/smoke.yaml.j2
new file mode 100644
index 00000000..4598ddf3
--- /dev/null
+++ b/tests/core/smoke.yaml.j2
@@ -0,0 +1,192 @@
+bundle: kubernetes
+
+applications:
+  traefik:
+    charm: ch:traefik-k8s
+    channel: 1.0/candidate
+    scale: 1
+    trust: true
+    options:
+      kubernetes-service-annotations: metallb.universe.tf/address-pool=public
+  mysql:
+    charm: ch:mysql-k8s
+    channel: 8.0/stable
+    scale: 1
+    trust: false
+  tls-operator:
+    charm: self-signed-certificates
+    channel: latest/beta
+    scale: 1
+    options:
+      ca-common-name: internal-ca
+  rabbitmq:
+    charm: ch:rabbitmq-k8s
+    channel: 3.12/edge
+    scale: 1
+    trust: true
+    options:
+      minimum-replicas: 1
+  ovn-central:
+    {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%}
+    charm: ../../../ovn-central-k8s.charm
+    {% else -%}
+    charm: ch:ovn-central-k8s
+    channel: 23.03/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09
+      ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09
+      ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09
+  ovn-relay:
+    {% if ovn_relay_k8s is defined and ovn_relay_k8s is sameas true -%}
+    charm: ../../../ovn-relay-k8s.charm
+    {% else -%}
+    charm: ch:ovn-relay-k8s
+    channel: 23.03/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09
+  keystone:
+    {% if keystone_k8s is defined and keystone_k8s is sameas true -%}
+    charm: ../../../keystone-k8s.charm
+    {% else -%}
+    charm: ch:keystone-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    options:
+      admin-role: admin
+    storage:
+      fernet-keys: 5M
+      credential-keys: 5M
+    resources:
+      keystone-image: ghcr.io/canonical/keystone:2023.2
+  glance:
+    {% if glance_k8s is defined and glance_k8s is sameas true -%}
+    charm: ../../../glance-k8s.charm
+    {% else -%}
+    charm: ch:glance-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    storage:
+      local-repository: 5G
+    resources:
+      glance-api-image: ghcr.io/canonical/glance-api:2023.2
+  nova:
+    {% if nova_k8s is defined and nova_k8s is sameas true -%}
+    charm: ../../../nova-k8s.charm
+    {% else -%}
+    charm: ch:nova-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      nova-api-image: ghcr.io/canonical/nova-consolidated:2023.2
+      nova-scheduler-image: ghcr.io/canonical/nova-consolidated:2023.2
+      nova-conductor-image: ghcr.io/canonical/nova-consolidated:2023.2
+  placement:
+    {% if placement_k8s is defined and placement_k8s is sameas true -%}
+    charm: ../../../placement-k8s.charm
+    {% else -%}
+    charm: ch:placement-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      placement-api-image: ghcr.io/canonical/placement-api:2023.2
+  neutron:
+    {% if neutron_k8s is defined and neutron_k8s is sameas true -%}
+    charm: ../../../neutron-k8s.charm
+    {% else -%}
+    charm: ch:neutron-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    options:
+      debug: true
+    resources:
+      neutron-server-image: ghcr.io/canonical/neutron-server:2023.2
+  horizon:
+    {% if horizon_k8s is defined and horizon_k8s is sameas true -%}
+    charm: ../../../horizon-k8s.charm
+    {% else -%}
+    charm: ch:horizon-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    resources:
+      horizon-image: ghcr.io/canonical/horizon:2023.2
+
+relations:
+- - tls-operator:certificates
+  - ovn-central:certificates
+
+- - tls-operator:certificates
+  - ovn-relay:certificates
+- - ovn-relay:ovsdb-cms
+  - ovn-central:ovsdb-cms
+
+- - mysql:database
+  - keystone:database
+- - traefik:ingress
+  - keystone:ingress-public
+
+- - mysql:database
+  - glance:database
+- - keystone:identity-service
+  - glance:identity-service
+- - rabbitmq:amqp
+  - glance:amqp
+- - traefik:ingress
+  - glance:ingress-public
+
+- - mysql:database
+  - nova:database
+- - mysql:database
+  - nova:api-database
+- - mysql:database
+  - nova:cell-database
+- - rabbitmq:amqp
+  - nova:amqp
+- - keystone:identity-service
+  - nova:identity-service
+- - traefik:ingress
+  - nova:ingress-public
+
+- - mysql:database
+  - placement:database
+- - keystone:identity-service
+  - placement:identity-service
+- - traefik:ingress
+  - placement:ingress-public
+
+- - mysql:database
+  - neutron:database
+- - rabbitmq:amqp
+  - neutron:amqp
+- - keystone:identity-service
+  - neutron:identity-service
+- - traefik:ingress
+  - neutron:ingress-public
+- - tls-operator:certificates
+  - neutron:certificates
+- - neutron:ovsdb-cms
+  - ovn-central:ovsdb-cms
+
+- - mysql:database
+  - horizon:database
+- - keystone:identity-credentials
+  - horizon:identity-credentials
+- - traefik:ingress
+  - horizon:ingress-public
diff --git a/tests/core/tests.yaml b/tests/core/tests.yaml
new file mode 100644
index 00000000..821a7d54
--- /dev/null
+++ b/tests/core/tests.yaml
@@ -0,0 +1,72 @@
+gate_bundles:
+  - smoke
+smoke_bundles:
+  - smoke
+configure:
+  - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints
+  - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
+  - zaza.openstack.charm_tests.nova.setup.create_flavors
+  - zaza.openstack.charm_tests.nova.setup.manage_ssh_key
+tests:
+  - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal
+tests_options:
+  trust:
+    - smoke
+  ignore_hard_deploy_errors:
+    - smoke
+
+  tempest:
+    default:
+      smoke: True
+      exclude-list:
+        - "tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file"
+        - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create"
+        - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list"
+        - "tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete"
+        - "tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON"
+        - "tempest.api.compute.servers.test_create_server.ServersTestManualDisk"
+        - "tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON"
+        - "tempest.api.compute.servers.test_create_server.ServersTestJSON"
+        - "tempest.scenario.test_server_multinode.TestServerMultinode.test_schedule_to_all_nodes"
+        - "tempest.scenario.test_server_basic_ops.TestServerBasicOps.test_server_basic_ops"
+        - "tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesUnderV243Test.test_add_remove_fixed_ip"
+      include-list:
+        - "tempest.api.identity.v3.test_application_credentials.ApplicationCredentialsV3Test.test_create_application_credential"
+
+target_deploy_status:
+  traefik:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  mysql:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  tls-operator:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  rabbitmq:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  ovn-central:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  ovn-relay:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  keystone:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  glance:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  nova:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  placement:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  neutron:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  horizon:
+    workload-status: active
+    workload-status-message-regex: '^$'
diff --git a/tests/misc/smoke.yaml.j2 b/tests/misc/smoke.yaml.j2
new file mode 100644
index 00000000..6ae7253a
--- /dev/null
+++ b/tests/misc/smoke.yaml.j2
@@ -0,0 +1,105 @@
+bundle: kubernetes
+
+applications:
+  traefik:
+    charm: ch:traefik-k8s
+    channel: 1.0/candidate
+    scale: 1
+    trust: true
+    options:
+      kubernetes-service-annotations: metallb.universe.tf/address-pool=public
+  mysql:
+    charm: ch:mysql-k8s
+    channel: 8.0/stable
+    scale: 1
+    trust: false
+  ldap-server:
+    charm: ch:ldap-test-fixture-k8s
+    channel: edge
+    scale: 1
+  rabbitmq:
+    charm: ch:rabbitmq-k8s
+    channel: 3.12/edge
+    scale: 1
+    trust: true
+    options:
+      minimum-replicas: 1
+  keystone:
+    {% if keystone_k8s is defined and keystone_k8s is sameas true -%}
+    charm: ../../../keystone-k8s.charm
+    {% else -%}
+    charm: ch:keystone-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: true
+    options:
+      admin-role: admin
+    storage:
+      fernet-keys: 5M
+      credential-keys: 5M
+    resources:
+      keystone-image: ghcr.io/canonical/keystone:2023.2
+  designate-bind:
+    {% if designate_bind_k8s is defined and designate_bind_k8s is sameas true -%}
+    charm: ../../../designate-bind-k8s.charm
+    {% else -%}
+    charm: ch:designate-bind-k8s
+    channel: 9/edge
+    {% endif -%}
+    scale: 1
+    trust: false
+    resources:
+      designate-bind-image: ubuntu/bind9:9.18-22.04_beta
+  designate:
+    {% if designate_k8s is defined and designate_k8s is sameas true -%}
+    charm: ../../../designate-k8s.charm
+    {% else -%}
+    charm: ch:designate-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    trust: false
+    resources:
+      designate-image: ghcr.io/canonical/designate-consolidated:2023.2
+  keystone-ldap:
+    {% if keystone_ldap_k8s is defined and keystone_ldap_k8s is sameas true -%}
+    charm: ../../../keystone-ldap-k8s.charm
+    {% else -%}
+    charm: ch:keystone-ldap-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+  openstack-exporter:
+    {% if openstack_exporter_k8s is defined and openstack_exporter_k8s is sameas true -%}
+    charm: ../../../openstack-exporter-k8s.charm
+    {% else -%}
+    charm: ch:openstack-exporter-k8s
+    channel: 2023.2/edge
+    {% endif -%}
+    scale: 1
+    resources:
+      openstack-exporter-image: ghcr.io/canonical/openstack-exporter:1.6.0-7533071
+
+relations:
+- - mysql:database
+  - keystone:database
+- - traefik:ingress
+  - keystone:ingress-public
+
+- - mysql:database
+  - designate:database
+- - rabbitmq:amqp
+  - designate:amqp
+- - keystone:identity-service
+  - designate:identity-service
+- - traefik:ingress
+  - designate:ingress-public
+- - designate-bind:dns-backend
+  - designate:dns-backend
+
+- - keystone:domain-config
+  - keystone-ldap:domain-config
+
+- - keystone:identity-ops
+  - openstack-exporter:identity-ops
diff --git a/tests/misc/tests.yaml b/tests/misc/tests.yaml
new file mode 100644
index 00000000..647232b4
--- /dev/null
+++ b/tests/misc/tests.yaml
@@ -0,0 +1,52 @@
+gate_bundles:
+  - smoke
+smoke_bundles:
+  - smoke
+configure:
+  - zaza.charm_tests.noop.setup.basic_setup
+  # https://bugs.launchpad.net/snap-openstack/+bug/2045206
+  # - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints
+tests:
+  - zaza.charm_tests.noop.tests.NoopTest
+  # Tests commented until bug fix for https://bugs.launchpad.net/snap-openstack/+bug/2045206
+  # - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal
+  # - zaza.openstack.charm_tests.keystone.tests_ldap_k8s.LdapExplicitCharmConfigTestsK8S
+  # - zaza.openstack.charm_tests.openstack_exporter.tests.OpenstackExporterTest
+tests_options:
+  trust:
+    - smoke
+  ignore_hard_deploy_errors:
+    - smoke
+
+  tempest:
+    default:
+      smoke: True
+
+target_deploy_status:
+  traefik:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  mysql:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  ldap-server:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  rabbitmq:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  keystone:
+    workload-status: waiting
+    workload-status-message-regex: '^.*domain-config.*integration incomplete.*$|^$'
+  designate-bind:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  designate:
+    workload-status: active
+    workload-status-message-regex: '^.*$'
+  keystone-ldap:
+    workload-status: active
+    workload-status-message-regex: '^$'
+  openstack-exporter:
+    workload-status: active
+    workload-status-message-regex: '^$'
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..2d2868ac
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,93 @@
+# Global tox file
+
+# This file is used to invoke tox in individual charms
+
+[tox]
+skipsdist = True
+envlist = pep8,py3
+sitepackages = False
+skip_missing_interpreters = False
+minversion = 3.18.0
+
+[testenv]
+passenv =
+  HOME
+allowlist_externals =
+  {toxinidir}/run_tox.sh
+  {toxinidir}/fetch_libs.sh
+
+[testenv:fetch]
+basepython = python3
+deps =
+commands =
+  {toxinidir}/fetch_libs.sh
+
+[testenv:fmt]
+description = Apply coding style standards to code
+deps =
+    black
+    isort
+commands =
+  {toxinidir}/run_tox.sh fmt
+
+[testenv:pep8]
+description = Alias for lint
+deps =
+    black
+    flake8<6
+    flake8-docstrings
+    flake8-copyright
+    flake8-builtins
+    pyproject-flake8
+    pep8-naming
+    isort
+    codespell
+commands =
+  {toxinidir}/run_tox.sh pep8
+
+[testenv:py3]
+deps =
+  -r{toxinidir}/test-requirements.txt
+commands =
+  {toxinidir}/run_tox.sh py3
+
+[testenv:py310]
+deps = {[testenv:py3]deps}
+commands =
+  {toxinidir}/run_tox.sh py310
+
+[testenv:py311]
+deps = {[testenv:py3]deps}
+commands =
+  {toxinidir}/run_tox.sh py311
+
+[testenv:cover]
+deps = {[testenv:py3]deps}
+commands =
+  {toxinidir}/run_tox.sh cover
+
+[testenv:build]
+basepython = python3
+deps =
+commands =
+  {toxinidir}/run_tox.sh build {posargs}
+
+[testenv:func-noop]
+basepython = python3
+deps =
+    git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
+    git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
+    git+https://opendev.org/openstack/tempest.git#egg=tempest
+commands =
+    functest-run-suite --help
+
+[testenv:func]
+basepython = python3
+deps = {[testenv:func-noop]deps}
+setenv =
+    TEST_MODEL_SETTINGS = automatically-retry-hooks=true
+    TEST_MAX_RESOLVE_COUNT = 5
+commands =
+    python3 render_bundles.py
+    # Example: functest-run-suite --keep-model --smoke --test-directory=tests/set1
+    functest-run-suite --keep-model {posargs}
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
new file mode 100644
index 00000000..648e8a3c
--- /dev/null
+++ b/zuul.d/jobs.yaml
@@ -0,0 +1,410 @@
+- job:
+    name: charm-build-keystone-k8s
+    description: Build sunbeam keystone-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/keystone-k8s/*
+    vars:
+      charm: keystone-k8s
+- job:
+    name: charm-build-glance-k8s
+    description: Build sunbeam glance-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/glance-k8s/*
+    vars:
+      charm: glance-k8s
+- job:
+    name: charm-build-nova-k8s
+    description: Build sunbeam nova-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/nova-k8s/*
+    vars:
+      charm: nova-k8s
+- job:
+    name: charm-build-placement-k8s
+    description: Build sunbeam placement-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/placement-k8s/*
+    vars:
+      charm: placement-k8s
+- job:
+    name: charm-build-neutron-k8s
+    description: Build sunbeam neutron-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/neutron-k8s/*
+    vars:
+      charm: neutron-k8s
+- job:
+    name: charm-build-ovn-central-k8s
+    description: Build sunbeam ovn-central-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/ovn-central-k8s/*
+    vars:
+      charm: ovn-central-k8s
+- job:
+    name: charm-build-ovn-relay-k8s
+    description: Build sunbeam ovn-relay-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/ovn-relay-k8s/*
+    vars:
+      charm: ovn-relay-k8s
+- job:
+    name: charm-build-cinder-k8s
+    description: Build sunbeam cinder-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/cinder-k8s/*
+    vars:
+      charm: cinder-k8s
+- job:
+    name: charm-build-cinder-ceph-k8s
+    description: Build sunbeam cinder-ceph-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/cinder-ceph-k8s/*
+    vars:
+      charm: cinder-ceph-k8s
+- job:
+    name: charm-build-horizon-k8s
+    description: Build sunbeam horizon-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/horizon-k8s/*
+    vars:
+      charm: horizon-k8s
+- job:
+    name: charm-build-heat-k8s
+    description: Build sunbeam heat-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/heat-k8s/*
+    vars:
+      charm: heat-k8s
+- job:
+    name: charm-build-octavia-k8s
+    description: Build sunbeam octavia-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/octavia-k8s/*
+    vars:
+      charm: octavia-k8s
+- job:
+    name: charm-build-aodh-k8s
+    description: Build sunbeam aodh-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/aodh-k8s/*
+    vars:
+      charm: aodh-k8s
+- job:
+    name: charm-build-ceilometer-k8s
+    description: Build sunbeam ceilometer-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/ceilometer-k8s/*
+    vars:
+      charm: ceilometer-k8s
+- job:
+    name: charm-build-gnocchi-k8s
+    description: Build sunbeam gnocchi-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/gnocchi-k8s/*
+    vars:
+      charm: gnocchi-k8s
+- job:
+    name: charm-build-barbican-k8s
+    description: Build sunbeam barbican-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/barbican-k8s/*
+    vars:
+      charm: barbican-k8s
+- job:
+    name: charm-build-magnum-k8s
+    description: Build sunbeam magnum-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/magnum-k8s/*
+    vars:
+      charm: magnum-k8s
+- job:
+    name: charm-build-designate-k8s
+    description: Build sunbeam designate-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/designate-k8s/*
+    vars:
+      charm: designate-k8s
+- job:
+    name: charm-build-designate-bind-k8s
+    description: Build sunbeam designate-bind-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/designate-bind-k8s/*
+    vars:
+      charm: designate-bind-k8s
+- job:
+    name: charm-build-keystone-ldap-k8s
+    description: Build sunbeam keystone-ldap-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/keystone-ldap-k8s/*
+    vars:
+      charm: keystone-ldap-k8s
+- job:
+    name: charm-build-openstack-exporter-k8s
+    description: Build sunbeam openstack-exporter-k8s charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/openstack-exporter-k8s/*
+    vars:
+      charm: openstack-exporter-k8s
+- job:
+    name: charm-build-openstack-hypervisor
+    description: Build sunbeam openstack-hypervisor charm
+    run: playbooks/charm/build.yaml
+    timeout: 3600
+    match-on-config-updates: false
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/openstack-hypervisor/*
+    vars:
+      charm: openstack-hypervisor
+
+- job:
+    name: func-test-core
+    description: |
+      Zaza smoke test for all the core sunbeam charms.
+    timeout: 3600
+    run: playbooks/zaza-func-test.yaml
+    post-run: playbooks/collect-run-data.yaml
+    dependencies:
+      - name: charm-build-keystone-k8s
+        soft: true
+      - name: charm-build-glance-k8s
+        soft: true
+      - name: charm-build-nova-k8s
+        soft: true
+      - name: charm-build-placement-k8s
+        soft: true
+      - name: charm-build-neutron-k8s
+        soft: true
+      - name: charm-build-ovn-central-k8s
+        soft: true
+      - name: charm-build-ovn-relay-k8s
+        soft: true
+      - name: charm-build-horizon-k8s
+        soft: true
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/keystone-k8s/*
+      - charms/glance-k8s/*
+      - charms/nova-k8s/*
+      - charms/neutron-k8s/*
+      - charms/placement-k8s/*
+      - charms/ovn-central-k8s/*
+      - charms/ovn-relay-k8s/*
+      - charms/horizon-k8s/*
+    vars:
+      # Artifacts will be downloaded from below charm jobs
+      charm_jobs:
+        - charm-build-keystone-k8s
+        - charm-build-glance-k8s
+        - charm-build-nova-k8s
+        - charm-build-placement-k8s
+        - charm-build-neutron-k8s
+        - charm-build-ovn-central-k8s
+        - charm-build-ovn-relay-k8s
+        - charm-build-horizon-k8s
+      # test_dir relative to project src dir
+      test_dir: tests/core
+- job:
+    name: func-test-ceph
+    description: |
+      Zaza smoke test for all the sunbeam charms that
+      requires storage/ceph.
+    timeout: 3600
+    run: playbooks/zaza-func-test.yaml
+    post-run: playbooks/collect-run-data.yaml
+    dependencies:
+      - name: charm-build-cinder-k8s
+        soft: true
+      - name: charm-build-cinder-ceph-k8s
+        soft: true
+      - name: charm-build-gnocchi-k8s
+        soft: true
+      - name: charm-build-ceilometer-k8s
+        soft: true
+      - name: charm-build-aodh-k8s
+        soft: true
+      - name: charm-build-keystone-k8s
+        soft: true
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/cinder-k8s/*
+      - charms/cinder-ceph-k8s/*
+      - charms/gnocchi-k8s/*
+      - charms/ceilometer-k8s/*
+      - charms/aodh-k8s/*
+    vars:
+      charm_jobs:
+        - charm-build-cinder-k8s
+        - charm-build-cinder-ceph-k8s
+        - charm-build-gnocchi-k8s
+        - charm-build-ceilometer-k8s
+        - charm-build-aodh-k8s
+        - charm-build-keystone-k8s
+      test_dir: tests/ceph
+- job:
+    name: func-test-caas
+    description: |
+      Zaza smoke test for magnum and dependent charms
+      like heat, octavia, barbican.
+    timeout: 3600
+    run: playbooks/zaza-func-test.yaml
+    post-run: playbooks/collect-run-data.yaml
+    dependencies:
+      - name: charm-build-heat-k8s
+        soft: true
+      - name: charm-build-octavia-k8s
+        soft: true
+      - name: charm-build-barbican-k8s
+        soft: true
+      - name: charm-build-magnum-k8s
+        soft: true
+      - name: charm-build-keystone-k8s
+        soft: true
+      - name: charm-build-glance-k8s
+        soft: true
+      - name: charm-build-ovn-central-k8s
+        soft: true
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/heat-k8s/*
+      - charms/octavia-k8s/*
+      - charms/barbican-k8s/*
+      - charms/magnum-k8s/*
+    vars:
+      charm_jobs:
+        - charm-build-heat-k8s
+        - charm-build-octavia-k8s
+        - charm-build-barbican-k8s
+        - charm-build-magnum-k8s
+        - charm-build-keystone-k8s
+        - charm-build-glance-k8s
+        - charm-build-ovn-central-k8s
+      test_dir: tests/caas
+- job:
+    name: func-test-misc
+    description: |
+      Zaza smoke test for designate, desginate-bind,
+      keystone-ldap, openstack-exporter charms.
+    timeout: 3600
+    run: playbooks/zaza-func-test.yaml
+    post-run: playbooks/collect-run-data.yaml
+    dependencies:
+      - name: charm-build-designate-k8s
+        soft: true
+      - name: charm-build-designate-bind-k8s
+        soft: true
+      - name: charm-build-keystone-k8s
+        soft: true
+      - name: charm-build-keystone-ldap-k8s
+        soft: true
+      - name: charm-build-openstack-exporter-k8s
+        soft: true
+    files:
+      - ops-sunbeam/ops_sunbeam/*
+      - charms/designate-k8s/*
+      - charms/designate-bind-k8s/*
+      - charms/keystone-ldap-k8s/*
+      - charms/openstack-exporter-k8s/*
+    vars:
+      charm_jobs:
+        - charm-build-designate-k8s
+        - charm-build-designate-bind-k8s
+        - charm-build-keystone-ldap-k8s
+        - charm-build-openstack-exporter-k8s
+        - charm-build-keystone-k8s
+      test_dir: tests/misc
+
+- job:
+    name: publish-charms
+    description: |
+      Publish all the charms built in the gate
+      pipeline.
+    post-review: true
+    run: playbooks/charm/publish.yaml
+    secrets:
+      charmhub_token
+    timeout: 3600
diff --git a/zuul.d/project-templates.yaml b/zuul.d/project-templates.yaml
new file mode 100644
index 00000000..5c22e69e
--- /dev/null
+++ b/zuul.d/project-templates.yaml
@@ -0,0 +1,137 @@
+- project-template:
+    name: openstack-python3-sunbeam-jobs
+    # NOTE(hemanth): This template is used in openstack sunbeam charms since
+    # stable/2023.1. The stable/2023.1 and stable/2023.2 charm branches
+    # support py310 unit tests and main support py310, py311 tests.
+    description: |
+      Runs unit tests for an OpenStack Sunbeam project under the CPython
+      version 3 releases designated for testing the latest release.
+    check:
+      jobs:
+        - openstack-tox-pep8
+        - openstack-tox-py310:
+            branches:
+              - stable/2023.1
+              - stable/2023.2
+              - main
+        - openstack-tox-py311:
+            branches:
+              - main
+    gate:
+      jobs:
+        - openstack-tox-pep8
+        - openstack-tox-py310:
+            branches:
+              - stable/2023.1
+              - stable/2023.2
+              - main
+        - openstack-tox-py311:
+            branches:
+              - main
+
+- project-template:
+    name: openstack-sunbeam-charm-build-jobs
+    description: |
+      Build the charms in OpenStack Sunbeam project.
+    check:
+      fail-fast: true
+      jobs:
+        - charm-build-keystone-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-glance-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-nova-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-placement-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-neutron-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ovn-central-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ovn-relay-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-cinder-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-cinder-ceph-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-horizon-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-heat-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-octavia-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-aodh-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ceilometer-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-gnocchi-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-barbican-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-magnum-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-designate-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-designate-bind-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-keystone-ldap-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-openstack-exporter-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-openstack-hypervisor:
+            nodeset: ubuntu-focal
+    gate:
+      fail-fast: true
+      jobs:
+        - charm-build-keystone-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-glance-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-nova-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-placement-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-neutron-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ovn-central-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ovn-relay-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-cinder-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-cinder-ceph-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-horizon-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-heat-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-octavia-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-aodh-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-ceilometer-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-gnocchi-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-barbican-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-magnum-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-designate-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-designate-bind-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-keystone-ldap-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-openstack-exporter-k8s:
+            nodeset: ubuntu-focal
+        - charm-build-openstack-hypervisor:
+            nodeset: ubuntu-focal
+
+- project-template:
+    name: charm-publish-jobs
+    description: |
+      The set of publish jobs for the OpenStack Sunbeam Charms
+    promote:
+      jobs:
+        - publish-charms
diff --git a/zuul.d/secrets.yaml b/zuul.d/secrets.yaml
new file mode 100644
index 00000000..45abce9d
--- /dev/null
+++ b/zuul.d/secrets.yaml
@@ -0,0 +1,65 @@
+- secret:
+    name: charmhub_token
+    data:
+      # Generated on 27-Nov-2023 with 90 days ttl
+      value: !encrypted/pkcs1-oaep
+        - oWjNK48hexdzzazFW8SA6M3otdZtNYV3cYABxa+q0NPog347A4jzdH0vhwI/wO8X0H6l0
+          ohgH54pI7jjfFGuG46LTZWgWyw7YET/jI7r025Oz9yiDl5178Zk0foPrTWjRnF0rQdKA3
+          V90Tmfn8xhHabrn2T5CFUrhvNnUQHxRMKy77jA/1hjmexC8E5NA2ypBIpPyOhwrOcyLZ8
+          t6PsgiSRiSxr7d8eWZBeBAo3LU3KFTBGCsBWp/kzVQCzItRN7SbCf5i8JXHPUySnyRCYT
+          UzMeb2FtlAf98Ng4SyGXys1iQ4zKn75PDgWp4FeJyociDCDlnuAlGmh3GQ+MkXWAPl+vU
+          RYtqnvkvhNl2p3rRTzUUlrrttTpWhj4Cp41whOgoe6ZLPaRjboXizQClp8qbGGIdgb980
+          aiTcFegNZl2RRersfgSwIqq+ys+SgPkQ1kFXpFDuLeCqL/OTEP3fBYAE4h3iIsmo0LSWH
+          /I9Oe8LQr1TQUEAlUNKMNIhlC8nL5uUOQXxHAmmIVQHNZYF1tObehMnFCWvFEWh3C5CGD
+          QgrT5zvTHUoH3GOfhDTEMH2ssft9rRJgcUieNlGvfMiypoJvQL51vfTEOHgUqBfqF6FCs
+          6FP/eewHUFxx00HyQmRSspRbnq6u7iTcWsZFOrY/Szo62EhEOHgKqMedpEGbRo=
+        - YIPNXBTQ65iqKpDbI8fKSG4gwYi58ORqCIrHPOiyGjuQiHbwFeExzZYtuFxPB7EYRJPwJ
+          0rrkBF434z7ySjovdyBwtB2svHLqcreB5RYIc4hWXzQN1QRUuSi+j79IQ27EAmkryY3OY
+          dR504mVOIBkLBKgS3ETiWvVLiOcHCiZD6mnq/MnLFg1xDrWb1aGQujBjeUDMDK1I7Qay8
+          wY+3MGnrsmqZjmrdzN7loUp/TxR9rtXWFKmIXLyKGeadSOTIIh/6nVQJfM72NUWcYpDUh
+          22EonzEZ0l9f+rWNeGx5ww8X2BRffSfeOPaJZSp3gMfljuA5o0HEAlTKTR0uA1iKp7iVR
+          BAW91yZPdIGk2oYA9ytfjx7LZCK6Xxax2dxtaHoqyBm9jeyrJ1p6IbiqRDp7lP3IsuCbe
+          g1iXItY6IhF8kthUFUTxQP/yf94KmQR8tY8cpEhXPhCEaQLitW67fIlkc9grGwIg644IY
+          Rqd5TCxB2FQ9N71aK9smcTMfQZbNXqH4n9Ix3KcAaknnKEuWCR5+A7bKXG2VIsofl3cc5
+          QmvyBzACoSvIeJHB73YABlesCKxBuBZX2lQQv3GB/KFilxtRAw6oPpZY744F9k1MFNO/R
+          NuZQJ3yVCq3QkWpM66kE774zR+3ZlQ639zTWZOXeZsjzZHuBSzRhnlezCk35mA=
+        - N7LtSHy4KoQiGMTcdZGDdawEB2BdbDQZabzQu0NnkW5kYMQ/hFqoRwPjLTB1mC0f/uJYy
+          jIgdQNX8uIXn6eAAuyaID9CJb9QO6rQ6+ZAHtj5FIpiCIoykEW4XpIZrIGFYWlgCjn87f
+          UDQgX3s5JUU5PAGUzaw2EWL3RtHgmHX6/Syh+tTS77qA1jam9edf5TwQ1JbXMERsdnl+k
+          fjs+THAzU7kt3XuMm08juZBkEvKP8lS87hmmYJWWDdpyxhph6FIhJ3LTOhJmQmN9ThUOL
+          kDOh5SueP9FOXF+2ozrJkq+PpcJQswZLiftKS54WSeFsIASeneGH1ZpVaMUKrE0XfFo5e
+          RVkUZC2MWJw0K4rDZmtBD0DD3LXfN22l3Pq0CJJb2p0pFbJ/x8+Xp7KCmuPYt559oeyko
+          H0ZMsxYlVgVwPB+6mkhck7UYZlGKpMZlvE7PTRCMHQ2bLT+GMGdx6vcHYJP/DQDvsf/H+
+          z1j0K5HfjlU6RM4GNrbT2xAxyCDsdFfsB0sevjLr3HAsTCzBw2AYf9VoNGlGCVFqt2Sru
+          CQD138fDI27KcwO+rVPoJsmrZEGMDhUZDhZVySZ80xTojDOMHF3RcYDcfnOOt22kQiZJD
+          8bNIw3E1++l+tv9d3Ki14Bpwlkm2SPw4mMPGHb4xxo15TYu/MPIp0CD18K1jCM=
+        - FtedxbTxdhPCJSwkx94pTapAwR1wbSABS/RMsluoNzxL74sv+xyNCkHVhSSTijPCEs5GH
+          +xPK4TVV2h065j8B2AlYmj2QspZc2F0BmhVLFQXU1k0yeGvnE2aID1WXrXteh9m6dW8i7
+          NHVcnuFuvZDL0/zAbMhn2kQ0SycyQ11z/ZPil2Q3RA9EMSCjI16QSJ2HrVg6UDiKmBTSI
+          c+J8mrT2Fw1ChlHQvea/x0BH/piFoFWGJYOLo63v4eCmpfvaZbkzOUqjgE6RA4w40FtTv
+          UAnIM6NidNoFnneOCYRxmem8PBYO6HbeCh11NoS69cfT47Jb7u/JoEL4cCqAdFZDUmVnd
+          zelgu4RG/2G2fQ90+Co3f7wJ10INlw3R22SUMf4CpotSyxiEpWDEmwn1M1GjjKKJiSq/X
+          WCY0ZHxkipcMdr6g0rgZ9a6Ousc10W+/5IKKLxgEvqdRX32U8tSvvOXC6zGGbxTjvSsbL
+          RvjbOYXt93caowz1uXtKrerqC545f6yiS9rDCOwMN0KQemRvP8J42Rdik/MgUsWuGhlZs
+          neCGIWHdNLfLSAInSHrYLw5KiSmBAfHuVjvVJsLXsw7fNzkl3PorxjJX3ypOe8xoT7o+V
+          xdoqdxgLxpR81OEs4Zsqx4tkSuVijUlPk3UUl224uqKsAOVjLKdbLLF0PIV4u8=
+        - iAneLfeh/CYMYf1ilfpDERn5KdPEgjYcQZRsFV1iw58Gvn8nQgXj7YB0iHZIT32Jn2JnO
+          +duKUh0aqHcrlf44cHBqQkq5EkbyR4prA0fHoO7Y8e16gAhkfmWhm2yr48XD0HAuSk6ID
+          PTvOw60T+aS3yafJzI7H91HqVVol1kM9zwwLCDIsx6OG5EFh6P98WYLNZH4isyWEXvgAj
+          ot3AR6kO//bXQLy1k+p4ECCVx3N9PtdoodehX2m9PUwx/VkXfNiJx53arsTPZe3mTcDCC
+          EZC/Lp73j2F68uQgwSyKQeQnrxaq0OwGbxzetarjheZLPH8ilB4AthnP29rn+y3Ydp37V
+          WVhV8O4zm+nbl7bsKF7BtadJy3TxR865wl8zLI7XtA+K7SZ4SpEHH0qiy8zRSlBLTTil6
+          E0li2xMPkyO8QC4I2Sbvj7LqfpfbK40UVXdte/nu0hpOw5rBxh4wp8l8jGUq8XYliJqkv
+          g4dvFZXeHTTm9IKft5LYZUUUIW2FZyg6Ds1qHkA0WFBNK2hymabgmN27R7srk2y0A5UV9
+          17VAo1zx7TniLlc3P163eLBR4xaNNOVyVcmSF7UJWKwg/Al6mRLuteDJrZUlWfZ+EmMe0
+          m9WG9y3RElsqvFabkIoUr0s95eZHv6IG+wo/4wpn7wzFJl05rsxlpeT/y8X/AI=
+        - TFcU1ReK6vwjV3JBQa3mufB8GObXLreHIf7Ziu+rZ1rAe9O1eymPjtS1YiCmny8Jo4zRy
+          EN/BLuWljfGrQKRwfP3FJa/oOcRq8m9l6uGNsu/qs4aSEPoqWhSum6FJKII/1WDpY1619
+          ckbUsFxyXWYbgS0Z65S5XGFHi4zUkAm2F8askpE3fmZ18kiTxw8evh99nfQWhblXAqEuO
+          yIyR+FpAXnHnnBBXpOI1Q+h+rfPQ9wACOo8LQKkEhZMWVh0mgNjlOifOS4ufnUlp+b87P
+          whDyahW2ZbWt2E9Zt+S3PheL6EQYO4WOjM/IyExj2dlViv2oTMs+NhK29I4e7e/danDon
+          +HY5yEOhvl3ecJNXxQQlW2QZE9RQfJQ5xAGQkUMIdwlJqeC0E8KAtYmVylE+D3k7gzQaY
+          /vvlqAA7YzRmqF5s6DkTN/FyOnd2IIVCxJvkCsudWzvegX9FUpIUg1UFhHtEKfd1N/ab6
+          DmlpWzg7SivW4Vk07BN9nR/ONa0+B7c5VmQ0POH2VZaIVbalG18DKkoVZqMxDRB+MS1Hf
+          XUzhH6kz9zTyUcDfXY76VDQFiriL2IzlDyCWk+xvXSuEIu8EEfilNr0l6GgBppvKogb2y
+          HxZtKfLQNQrBszndmq6QLc0+6nPa8M6egt7RbeRgxyrWqqM6m9MfPVDKjOmw78=
diff --git a/zuul.d/zuul.yaml b/zuul.d/zuul.yaml
new file mode 100644
index 00000000..4eabeb1c
--- /dev/null
+++ b/zuul.d/zuul.yaml
@@ -0,0 +1,48 @@
+- project:
+    templates:
+      - openstack-python3-sunbeam-jobs
+      - openstack-cover-jobs
+      - openstack-sunbeam-charm-build-jobs
+    check:
+      jobs:
+        - func-test-core:
+            nodeset: ubuntu-focal
+            voting: false
+        - func-test-ceph:
+            nodeset: ubuntu-focal
+            voting: false
+        - func-test-caas:
+            nodeset: ubuntu-focal
+            voting: false
+        - func-test-misc:
+            nodeset: ubuntu-focal
+            voting: false
+    vars:
+      juju_channel: 3.2/stable
+      juju_classic_mode: false
+      microk8s_channel: 1.28-strict/stable
+      microk8s_classic_mode: false
+      charmcraft_channel: 2.0/stable
+      publish_channels:
+        keystone-k8s: 2023.2/edge
+        glance-k8s: 2023.2/edge
+        nova-k8s: 2023.2/edge
+        placement-k8s: 2023.2/edge
+        neutron-k8s: 2023.2/edge
+        ovn-central-k8s: 23.09/edge
+        ovn-relay-k8s: 23.09/edge
+        cinder-k8s: 2023.2/edge
+        cinder-ceph-k8s: 2023.2/edge
+        horizon-k8s: 2023.2/edge
+        heat-k8s: 2023.2/edge
+        octavia-k8s: 2023.2/edge
+        aodh-k8s: 2023.2/edge
+        ceilometer-k8s: 2023.2/edge
+        gnocchi-k8s: 2023.2/edge
+        barbican-k8s: 2023.2/edge
+        designate-k8s: 2023.2/edge
+        designate-bind-k8s: 9/edge
+        magnum-k8s: 2023.2/edge
+        keystone-ldap-k8s: 2023.2/edge
+        openstack-exporter-k8s: 2023.2/edge
+        openstack-hypervisor: 2023.2/edge