LCM sidecar charm
[osm/devops.git] / installers / charm / osm-lcm / lib / charms / osm_vca_integrator / v0 / vca.py
1 # Copyright 2022 Canonical Ltd.
2 # See LICENSE file for licensing details.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13 # implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """VCA Library.
18
19 VCA stands for VNF Configuration and Abstraction, and is one of the core components
20 of OSM. The Juju Controller is in charged of this role.
21
22 This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
23 `vca` [interface](https://juju.is/docs/sdk/relations).
24
25 The *provider* side of this interface is implemented by the
26 [osm-vca-integrator Charmed Operator](https://charmhub.io/osm-vca-integrator).
27
28 helps to integrate with the
29 vca-integrator charm, which provides data needed to the OSM components that need
30 to talk to the VCA, and
31
32 Any Charmed OSM component that *requires* to talk to the VCA should implement
33 the *requirer* side of this interface.
34
35 In a nutshell using this library to implement a Charmed Operator *requiring* VCA data
36 would look like
37
38 ```
39 $ charmcraft fetch-lib charms.osm_vca_integrator.v0.vca
40 ```
41
42 `metadata.yaml`:
43
44 ```
45 requires:
46 vca:
47 interface: osm-vca
48 ```
49
50 `src/charm.py`:
51
52 ```
53 from charms.osm_vca_integrator.v0.vca import VcaData, VcaIntegratorEvents, VcaRequires
54 from ops.charm import CharmBase
55
56
57 class MyCharm(CharmBase):
58
59 on = VcaIntegratorEvents()
60
61 def __init__(self, *args):
62 super().__init__(*args)
63 self.vca = VcaRequires(self)
64 self.framework.observe(
65 self.on.vca_data_changed,
66 self._on_vca_data_changed,
67 )
68
69 def _on_vca_data_changed(self, event):
70 # Get Vca data
71 data: VcaData = self.vca.data
72 # data.endpoints => "localhost:17070"
73 ```
74
75 You can file bugs
76 [here](https://github.com/charmed-osm/osm-vca-integrator-operator/issues)!
77 """
78
79 import json
80 import logging
81 from typing import Any, Dict, Optional
82
83 from ops.charm import CharmBase, CharmEvents, RelationChangedEvent
84 from ops.framework import EventBase, EventSource, Object
85
86 # The unique Charmhub library identifier, never change it
87 from ops.model import Relation
88
89 # The unique Charmhub library identifier, never change it
90 LIBID = "746b36c382984e5c8660b78192d84ef9"
91
92 # Increment this major API version when introducing breaking changes
93 LIBAPI = 0
94
95 # Increment this PATCH version before using `charmcraft publish-lib` or reset
96 # to 0 if you are raising the major API version
97 LIBPATCH = 3
98
99
100 logger = logging.getLogger(__name__)
101
102
103 class VcaDataChangedEvent(EventBase):
104 """Event emitted whenever there is a change in the vca data."""
105
106 def __init__(self, handle):
107 super().__init__(handle)
108
109
110 class VcaIntegratorEvents(CharmEvents):
111 """VCA Integrator events.
112
113 This class defines the events that ZooKeeper can emit.
114
115 Events:
116 vca_data_changed (_VcaDataChanged)
117 """
118
119 vca_data_changed = EventSource(VcaDataChangedEvent)
120
121
122 RELATION_MANDATORY_KEYS = ("endpoints", "user", "secret", "public-key", "cacert", "model-configs")
123
124
125 class VcaData:
126 """Vca data class."""
127
128 def __init__(self, data: Dict[str, Any]) -> None:
129 self.data: str = data
130 self.endpoints: str = data["endpoints"]
131 self.user: str = data["user"]
132 self.secret: str = data["secret"]
133 self.public_key: str = data["public-key"]
134 self.cacert: str = data["cacert"]
135 self.lxd_cloud: str = data.get("lxd-cloud")
136 self.lxd_credentials: str = data.get("lxd-credentials")
137 self.k8s_cloud: str = data.get("k8s-cloud")
138 self.k8s_credentials: str = data.get("k8s-credentials")
139 self.model_configs: Dict[str, Any] = data.get("model-configs", {})
140
141
142 class VcaDataMissingError(Exception):
143 """Data missing exception."""
144
145
146 class VcaRequires(Object):
147 """Requires part of the vca relation.
148
149 Attributes:
150 endpoint_name: Endpoint name of the charm for the vca relation.
151 data: Vca data from the relation.
152 """
153
154 def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None:
155 super().__init__(charm, endpoint_name)
156 self._charm = charm
157 self.endpoint_name = endpoint_name
158 self.framework.observe(charm.on[endpoint_name].relation_changed, self._on_relation_changed)
159
160 @property
161 def data(self) -> Optional[VcaData]:
162 """Vca data from the relation."""
163 relation: Relation = self.model.get_relation(self.endpoint_name)
164 if not relation or relation.app not in relation.data:
165 logger.debug("no application data in the event")
166 return
167
168 relation_data: Dict = dict(relation.data[relation.app])
169 relation_data["model-configs"] = json.loads(relation_data.get("model-configs", "{}"))
170 try:
171 self._validate_relation_data(relation_data)
172 return VcaData(relation_data)
173 except VcaDataMissingError as e:
174 logger.warning(e)
175
176 def _on_relation_changed(self, event: RelationChangedEvent) -> None:
177 if event.app not in event.relation.data:
178 logger.debug("no application data in the event")
179 return
180
181 relation_data = event.relation.data[event.app]
182 try:
183 self._validate_relation_data(relation_data)
184 self._charm.on.vca_data_changed.emit()
185 except VcaDataMissingError as e:
186 logger.warning(e)
187
188 def _validate_relation_data(self, relation_data: Dict[str, str]) -> None:
189 if not all(required_key in relation_data for required_key in RELATION_MANDATORY_KEYS):
190 raise VcaDataMissingError("vca data not ready yet")
191
192 clouds = ("lxd-cloud", "k8s-cloud")
193 if not any(cloud in relation_data for cloud in clouds):
194 raise VcaDataMissingError("no clouds defined yet")
195
196
197 class VcaProvides(Object):
198 """Provides part of the vca relation.
199
200 Attributes:
201 endpoint_name: Endpoint name of the charm for the vca relation.
202 """
203
204 def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None:
205 super().__init__(charm, endpoint_name)
206 self.endpoint_name = endpoint_name
207
208 def update_vca_data(self, vca_data: VcaData) -> None:
209 """Update vca data in relation.
210
211 Args:
212 vca_data: VcaData object.
213 """
214 relation: Relation
215 for relation in self.model.relations[self.endpoint_name]:
216 if not relation or self.model.app not in relation.data:
217 logger.debug("relation app data not ready yet")
218 for key, value in vca_data.data.items():
219 if key == "model-configs":
220 value = json.dumps(value)
221 relation.data[self.model.app][key] = value