From bb6415948df2dc6a016a2bef4686ebb93c9c68e1 Mon Sep 17 00:00:00 2001 From: David Garcia Date: Tue, 14 Jun 2022 17:43:09 +0200 Subject: [PATCH] Add NBI Charm Library Change-Id: I7dbfa487918b05e0bf98776db1b1cec5586beec7 Signed-off-by: David Garcia --- .../osm-nbi/lib/charms/osm_nbi/v0/nbi.py | 185 ++++++++++++++++++ installers/charm/osm-nbi/src/charm.py | 12 +- .../charm/osm-nbi/tests/unit/test_charm.py | 10 + installers/charm/osm-nbi/tox.ini | 6 +- 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py 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 index 00000000..2def702a --- /dev/null +++ b/installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py @@ -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) diff --git a/installers/charm/osm-nbi/src/charm.py b/installers/charm/osm-nbi/src/charm.py index 964050a9..162e2a5a 100755 --- a/installers/charm/osm-nbi/src/charm.py +++ b/installers/charm/osm-nbi/src/charm.py @@ -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 diff --git a/installers/charm/osm-nbi/tests/unit/test_charm.py b/installers/charm/osm-nbi/tests/unit/test_charm.py index 82626efc..d9440a06 100644 --- a/installers/charm/osm-nbi/tests/unit/test_charm.py +++ b/installers/charm/osm-nbi/tests/unit/test_charm.py @@ -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 diff --git a/installers/charm/osm-nbi/tox.ini b/installers/charm/osm-nbi/tox.ini index 6c3980ea..b791e14b 100644 --- a/installers/charm/osm-nbi/tox.ini +++ b/installers/charm/osm-nbi/tox.ini @@ -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 -- 2.17.1