Add NBI Charm Library 02/12202/2
authorDavid Garcia <david.garcia@canonical.com>
Tue, 14 Jun 2022 15:43:09 +0000 (17:43 +0200)
committergarciadav <david.garcia@canonical.com>
Wed, 15 Jun 2022 06:20:25 +0000 (08:20 +0200)
Change-Id: I7dbfa487918b05e0bf98776db1b1cec5586beec7
Signed-off-by: David Garcia <david.garcia@canonical.com>
installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py [new file with mode: 0644]
installers/charm/osm-nbi/src/charm.py
installers/charm/osm-nbi/tests/unit/test_charm.py
installers/charm/osm-nbi/tox.ini

diff --git a/installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py b/installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py
new file mode 100644 (file)
index 0000000..2def702
--- /dev/null
@@ -0,0 +1,185 @@
+#!/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
+
+"""Nbi library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`nbi` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[osm-nbi Charmed Operator](https://charmhub.io/osm-nbi).
+
+Any Charmed Operator that *requires* NBI for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+NBI would look like
+
+```
+$ charmcraft fetch-lib charms.osm_nbi.v0.nbi
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  nbi:
+    interface: nbi
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.osm_nbi.v0.nbi import NbiRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.nbi = NbiRequires(self)
+        self.framework.observe(
+            self.on["nbi"].relation_changed,
+            self._on_nbi_relation_changed,
+        )
+        self.framework.observe(
+            self.on["nbi"].relation_broken,
+            self._on_nbi_relation_broken,
+        )
+        self.framework.observe(
+            self.on["nbi"].relation_broken,
+            self._on_nbi_broken,
+        )
+
+    def _on_nbi_relation_broken(self, event):
+        # Get NBI host and port
+        host: str = self.nbi.host
+        port: int = self.nbi.port
+        # host => "osm-nbi"
+        # port => 9999
+
+    def _on_nbi_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need nbi 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, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+from ops.model import Relation
+
+
+# The unique Charmhub library identifier, never change it
+LIBID = "8c888f7c869949409e12c16d78ec068b"
+
+# 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
+
+NBI_HOST_APP_KEY = "host"
+NBI_PORT_APP_KEY = "port"
+
+
+class NbiRequires(Object):  # pragma: no cover
+    """Requires-side of the Nbi relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    @property
+    def host(self) -> str:
+        """Get nbi hostname."""
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(NBI_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        """Get nbi port number."""
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(NBI_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class NbiProvides(Object):
+    """Provides-side of the Nbi relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> 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 Nbi host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Nbi hostname or IP address.
+            port (int): Nbi 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][NBI_HOST_APP_KEY] = host
+        relation.data[self.model.app][NBI_PORT_APP_KEY] = str(port)
index 964050a..162e2a5 100755 (executable)
@@ -40,8 +40,9 @@ from charms.osm_libs.v0.utils import (
     check_container_ready,
     check_service_active,
 )
+from charms.osm_nbi.v0.nbi import NbiProvides
 from lightkube.models.core_v1 import ServicePort
-from ops.charm import ActionEvent, CharmBase
+from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent
 from ops.framework import StoredState
 from ops.main import main
 from ops.model import ActiveStatus, Container
@@ -80,6 +81,7 @@ class OsmNbiCharm(CharmBase):
             },
         )
         self.kafka = KafkaRequires(self)
+        self.nbi = NbiProvides(self)
         self.mongodb_client = MongoClient(self, "mongodb")
         self.prometheus_client = PrometheusClient(self, "prometheus")
         self.keystone_client = KeystoneClient(self, "keystone")
@@ -113,7 +115,7 @@ class OsmNbiCharm(CharmBase):
 
             self._configure_service(self.container)
             self._update_ingress_config()
-
+            self._update_nbi_relation()
             # Update charm status
             self._on_update_status()
         except CharmError as e:
@@ -145,6 +147,11 @@ class OsmNbiCharm(CharmBase):
         finally:
             self._on_update_status()
 
+    def _update_nbi_relation(self, event: RelationJoinedEvent = None) -> None:
+        """Handler for the nbi-relation-joined event."""
+        if self.unit.is_leader():
+            self.nbi.set_host_info(self.app.name, SERVICE_PORT, event.relation if event else None)
+
     def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
         """Handler for the get-debug-mode-information action event."""
         if not self.debug_mode.started:
@@ -173,6 +180,7 @@ class OsmNbiCharm(CharmBase):
             self.on["kafka"].relation_broken: self._on_required_relation_broken,
             # Action events
             self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
+            self.on.nbi_relation_joined: self._update_nbi_relation,
         }
         for relation in [self.on[rel_name] for rel_name in ["mongodb", "prometheus", "keystone"]]:
             event_handler_mapping[relation.relation_changed] = self._on_config_changed
index 82626ef..d9440a0 100644 (file)
@@ -65,6 +65,16 @@ def test_container_stops_after_relation_broken(harness: Harness):
         check_service_active(container, service_name)
 
 
+def test_nbi_relation_joined(harness: Harness):
+    harness.set_leader(True)
+    _add_relations(harness)
+    relation_id = harness.add_relation("nbi", "ng-ui")
+    harness.add_relation_unit(relation_id, "ng-ui/0")
+    relation_data = harness.get_relation_data(relation_id, harness.charm.app.name)
+    assert harness.charm.unit.status == ActiveStatus()
+    assert relation_data == {"host": harness.charm.app.name, "port": "9999"}
+
+
 def _add_relations(harness: Harness):
     relation_ids = []
     # Add mongo relation
index 6c3980e..b791e14 100644 (file)
@@ -26,7 +26,7 @@ envlist = lint, unit
 [vars]
 src_path = {toxinidir}/src/
 tst_path = {toxinidir}/tests/
-;lib_path = {toxinidir}/lib/charms/operator_name_with_underscores
+lib_path = {toxinidir}/lib/charms/osm_nbi
 all_path = {[vars]src_path} {[vars]tst_path} 
 
 [testenv]
@@ -62,7 +62,7 @@ deps =
     codespell
 commands =
     # uncomment the following line if this charm owns a lib
-    codespell {[vars]lib_path}
+    codespell {[vars]lib_path}
     codespell {toxinidir}/. --skip {toxinidir}/.git --skip {toxinidir}/.tox \
       --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \
       --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg
@@ -79,7 +79,7 @@ deps =
     coverage[toml]
     -r{toxinidir}/requirements.txt
 commands =
-    coverage run --source={[vars]src_path} \
+    coverage run --source={[vars]src_path},{[vars]lib_path} \
         -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
     coverage report
     coverage xml