Temporal Charm
[osm/devops.git] / installers / charm / osm-nbi / lib / charms / osm_temporal / v0 / temporal.py
diff --git a/installers/charm/osm-nbi/lib/charms/osm_temporal/v0/temporal.py b/installers/charm/osm-nbi/lib/charms/osm_temporal/v0/temporal.py
new file mode 100644 (file)
index 0000000..2fc2fe2
--- /dev/null
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: legal@canonical.com
+#
+# To get in touch with the maintainers, please contact:
+# osm-charmers@lists.launchpad.net
+#
+#
+# Learn more at: https://juju.is/docs/sdk
+
+"""Temporal Frontend library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`temporal` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[osm-temporal Charmed Operator](https://charmhub.io/osm-temporal).
+
+Any Charmed Operator that *requires* Temporal for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Temporal would look like
+
+```
+$ charmcraft fetch-lib charms.osm_temporal.v0.temporal
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  temporal:
+    interface: frontend
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.osm_temporal.v0.temporal import TemporalRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.temporal = TemporalRequires(self)
+        self.framework.observe(
+            self.on["temporal"].relation_changed,
+            self._on_temporal_relation_changed,
+        )
+        self.framework.observe(
+            self.on["temporal"].relation_broken,
+            self._on_temporal_relation_broken,
+        )
+        self.framework.observe(
+            self.on["temporal"].relation_broken,
+            self._on_temporal_broken,
+        )
+
+    def _on_temporal_relation_broken(self, event):
+        # Get TEMPORAL host and port
+        host: str = self.temporal.host
+        port: int = self.temporal.port
+        # host => "osm-temporal"
+        # port => 7233
+
+    def _on_temporal_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need temporal relation")
+```
+
+You can file bugs
+[here](https://osm.etsi.org/bugzilla/enter_bug.cgi), selecting the `devops` module!
+"""
+from typing import Optional
+
+from ops.charm import CharmBase
+from ops.framework import Object
+from ops.model import Relation
+
+
+# The unique Charmhub library identifier, never change it
+LIBID = "5174d817d46c4e159c1a90ff8303d96a"
+
+# 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
+
+TEMPORAL_HOST_APP_KEY = "host"
+TEMPORAL_PORT_APP_KEY = "port"
+
+
+class TemporalRequires(Object):  # pragma: no cover
+    """Requires-side of the Temporal relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "temporal") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+    @property
+    def host(self) -> str:
+        """Get temporal hostname."""
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(TEMPORAL_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        """Get temporal port number."""
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(TEMPORAL_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class TemporalProvides(Object):
+    """Provides-side of the Temporal relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "temporal") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Temporal host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Temporal hostname or IP address.
+            port (int): Temporal port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][TEMPORAL_HOST_APP_KEY] = host
+        relation.data[self.model.app][TEMPORAL_PORT_APP_KEY] = str(port)