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,
 )
     check_container_ready,
     check_service_active,
 )
+from charms.osm_nbi.v0.nbi import NbiProvides
 from lightkube.models.core_v1 import ServicePort
 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
 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.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")
         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._configure_service(self.container)
             self._update_ingress_config()
-
+            self._update_nbi_relation()
             # Update charm status
             self._on_update_status()
         except CharmError as e:
             # Update charm status
             self._on_update_status()
         except CharmError as e:
@@ -145,6 +147,11 @@ class OsmNbiCharm(CharmBase):
         finally:
             self._on_update_status()
 
         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:
     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["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
         }
         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)
 
 
         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
 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/
 [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]
 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
 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
     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[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
         -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
     coverage report
     coverage xml