LCM sidecar charm
[osm/devops.git] / installers / charm / osm-lcm / lib / charms / osm_vca_integrator / v0 / vca.py
diff --git a/installers/charm/osm-lcm/lib/charms/osm_vca_integrator/v0/vca.py b/installers/charm/osm-lcm/lib/charms/osm_vca_integrator/v0/vca.py
new file mode 100644 (file)
index 0000000..21dac69
--- /dev/null
@@ -0,0 +1,221 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""VCA Library.
+
+VCA stands for VNF Configuration and Abstraction, and is one of the core components
+of OSM. The Juju Controller is in charged of this role.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`vca` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[osm-vca-integrator Charmed Operator](https://charmhub.io/osm-vca-integrator).
+
+helps to integrate with the
+vca-integrator charm, which provides data needed to the OSM components that need
+to talk to the VCA, and
+
+Any Charmed OSM component that *requires* to talk to the VCA should implement
+the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring* VCA data
+would look like
+
+```
+$ charmcraft fetch-lib charms.osm_vca_integrator.v0.vca
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  vca:
+    interface: osm-vca
+```
+
+`src/charm.py`:
+
+```
+from charms.osm_vca_integrator.v0.vca import VcaData, VcaIntegratorEvents, VcaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = VcaIntegratorEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.vca = VcaRequires(self)
+        self.framework.observe(
+            self.on.vca_data_changed,
+            self._on_vca_data_changed,
+        )
+
+    def _on_vca_data_changed(self, event):
+        # Get Vca data
+        data: VcaData = self.vca.data
+        # data.endpoints => "localhost:17070"
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/osm-vca-integrator-operator/issues)!
+"""
+
+import json
+import logging
+from typing import Any, Dict, Optional
+
+from ops.charm import CharmBase, CharmEvents, RelationChangedEvent
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+# The unique Charmhub library identifier, never change it
+LIBID = "746b36c382984e5c8660b78192d84ef9"
+
+# 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 VcaDataChangedEvent(EventBase):
+    """Event emitted whenever there is a change in the vca data."""
+
+    def __init__(self, handle):
+        super().__init__(handle)
+
+
+class VcaIntegratorEvents(CharmEvents):
+    """VCA Integrator events.
+
+    This class defines the events that ZooKeeper can emit.
+
+    Events:
+        vca_data_changed (_VcaDataChanged)
+    """
+
+    vca_data_changed = EventSource(VcaDataChangedEvent)
+
+
+RELATION_MANDATORY_KEYS = ("endpoints", "user", "secret", "public-key", "cacert", "model-configs")
+
+
+class VcaData:
+    """Vca data class."""
+
+    def __init__(self, data: Dict[str, Any]) -> None:
+        self.data: str = data
+        self.endpoints: str = data["endpoints"]
+        self.user: str = data["user"]
+        self.secret: str = data["secret"]
+        self.public_key: str = data["public-key"]
+        self.cacert: str = data["cacert"]
+        self.lxd_cloud: str = data.get("lxd-cloud")
+        self.lxd_credentials: str = data.get("lxd-credentials")
+        self.k8s_cloud: str = data.get("k8s-cloud")
+        self.k8s_credentials: str = data.get("k8s-credentials")
+        self.model_configs: Dict[str, Any] = data.get("model-configs", {})
+
+
+class VcaDataMissingError(Exception):
+    """Data missing exception."""
+
+
+class VcaRequires(Object):
+    """Requires part of the vca relation.
+
+    Attributes:
+        endpoint_name: Endpoint name of the charm for the vca relation.
+        data: Vca data from the relation.
+    """
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None:
+        super().__init__(charm, endpoint_name)
+        self._charm = charm
+        self.endpoint_name = endpoint_name
+        self.framework.observe(charm.on[endpoint_name].relation_changed, self._on_relation_changed)
+
+    @property
+    def data(self) -> Optional[VcaData]:
+        """Vca data from the relation."""
+        relation: Relation = self.model.get_relation(self.endpoint_name)
+        if not relation or relation.app not in relation.data:
+            logger.debug("no application data in the event")
+            return
+
+        relation_data: Dict = dict(relation.data[relation.app])
+        relation_data["model-configs"] = json.loads(relation_data.get("model-configs", "{}"))
+        try:
+            self._validate_relation_data(relation_data)
+            return VcaData(relation_data)
+        except VcaDataMissingError as e:
+            logger.warning(e)
+
+    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
+        if event.app not in event.relation.data:
+            logger.debug("no application data in the event")
+            return
+
+        relation_data = event.relation.data[event.app]
+        try:
+            self._validate_relation_data(relation_data)
+            self._charm.on.vca_data_changed.emit()
+        except VcaDataMissingError as e:
+            logger.warning(e)
+
+    def _validate_relation_data(self, relation_data: Dict[str, str]) -> None:
+        if not all(required_key in relation_data for required_key in RELATION_MANDATORY_KEYS):
+            raise VcaDataMissingError("vca data not ready yet")
+
+        clouds = ("lxd-cloud", "k8s-cloud")
+        if not any(cloud in relation_data for cloud in clouds):
+            raise VcaDataMissingError("no clouds defined yet")
+
+
+class VcaProvides(Object):
+    """Provides part of the vca relation.
+
+    Attributes:
+        endpoint_name: Endpoint name of the charm for the vca relation.
+    """
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None:
+        super().__init__(charm, endpoint_name)
+        self.endpoint_name = endpoint_name
+
+    def update_vca_data(self, vca_data: VcaData) -> None:
+        """Update vca data in relation.
+
+        Args:
+            vca_data: VcaData object.
+        """
+        relation: Relation
+        for relation in self.model.relations[self.endpoint_name]:
+            if not relation or self.model.app not in relation.data:
+                logger.debug("relation app data not ready yet")
+            for key, value in vca_data.data.items():
+                if key == "model-configs":
+                    value = json.dumps(value)
+                relation.data[self.model.app][key] = value