2 #######################################################################################
3 # Copyright ETSI Contributors and Others.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #######################################################################################
19 """VcaIntegrator K8s charm module."""
25 from pathlib
import Path
26 from typing
import Dict
, Set
29 from charms
.osm_vca_integrator
.v0
.vca
import VcaData
, VcaProvides
30 from juju
.controller
import Controller
31 from ops
.charm
import CharmBase
32 from ops
.main
import main
33 from ops
.model
import ActiveStatus
, BlockedStatus
, StatusBase
35 logger
= logging
.getLogger(__name__
)
37 GO_COOKIES
= "/root/.go-cookies"
38 JUJU_DATA
= os
.environ
["JUJU_DATA"] = "/root/.local/share/juju"
40 "public-key": "ssh/juju_id_rsa.pub",
41 "controllers": "controllers.yaml",
42 "accounts": "accounts.yaml",
46 class CharmError(Exception):
47 """Charm Error Exception."""
49 def __init__(self
, message
: str, status_class
: StatusBase
= BlockedStatus
) -> None:
50 self
.message
= message
51 self
.status_class
= status_class
52 self
.status
= status_class(message
)
55 class VcaIntegratorCharm(CharmBase
):
56 """VcaIntegrator K8s Charm operator."""
58 def __init__(self
, *args
):
59 super().__init
__(*args
)
60 self
.vca_provider
= VcaProvides(self
)
61 # Observe charm events
62 event_observe_mapping
= {
63 self
.on
.config_changed
: self
._on
_config
_changed
,
64 self
.on
.vca_relation_joined
: self
._on
_config
_changed
,
66 for event
, observer
in event_observe_mapping
.items():
67 self
.framework
.observe(event
, observer
)
69 # ---------------------------------------------------------------------------
71 # ---------------------------------------------------------------------------
74 def clouds_set(self
) -> Set
:
75 """Clouds set in the configuration."""
77 for cloud_config
in ["k8s-cloud", "lxd-cloud"]:
78 if cloud_name
:= self
.config
.get(cloud_config
):
79 clouds_set
.add(cloud_name
.split(":")[0])
83 def vca_data(self
) -> VcaData
:
85 return VcaData(self
._get
_vca
_data
())
87 # ---------------------------------------------------------------------------
88 # Handlers for Charm Events
89 # ---------------------------------------------------------------------------
91 def _on_config_changed(self
, _
) -> None:
92 """Handler for the config-changed event."""
93 # Validate charm configuration
95 self
._validate
_config
()
96 self
._write
_controller
_config
_files
()
97 self
._check
_controller
()
98 self
.vca_provider
.update_vca_data(self
.vca_data
)
99 self
.unit
.status
= ActiveStatus()
100 except CharmError
as e
:
101 self
.unit
.status
= e
.status
103 # ---------------------------------------------------------------------------
104 # Validation and configuration
105 # ---------------------------------------------------------------------------
107 def _validate_config(self
) -> None:
108 """Validate charm configuration.
111 Exception: if charm configuration is invalid.
113 # Check mandatory fields
114 for mandatory_field
in [
119 if not self
.config
.get(mandatory_field
):
120 raise CharmError(f
'missing config: "{mandatory_field}"')
121 # Check if any clouds are set
122 if not self
.clouds_set
:
123 raise CharmError("no clouds set")
125 if self
.config
.get("model-configs"):
127 yaml
.safe_load(self
.config
["model-configs"])
129 raise CharmError("invalid yaml format for model-configs")
131 def _write_controller_config_files(self
) -> None:
132 Path(f
"{JUJU_DATA}/ssh").mkdir(parents
=True, exist_ok
=True)
133 go_cookies
= Path(GO_COOKIES
)
134 if not go_cookies
.is_file():
135 go_cookies
.write_text(data
="[]")
136 for config
, path
in JUJU_CONFIGS
.items():
137 Path(f
"{JUJU_DATA}/{path}").expanduser().write_text(self
.config
[config
])
139 def _check_controller(self
):
140 loop
= asyncio
.get_event_loop()
141 # Check controller connectivity
142 loop
.run_until_complete(self
._check
_controller
_connectivity
())
143 # Check clouds exist in controller
144 loop
.run_until_complete(self
._check
_clouds
_in
_controller
())
146 async def _check_controller_connectivity(self
):
147 controller
= Controller()
148 await controller
.connect()
149 await controller
.disconnect()
151 async def _check_clouds_in_controller(self
):
152 controller
= Controller()
153 await controller
.connect()
155 controller_clouds
= await controller
.clouds()
156 for cloud
in self
.clouds_set
:
157 if f
"cloud-{cloud}" not in controller_clouds
.clouds
:
158 raise CharmError(f
"Cloud {cloud} does not exist in the controller")
160 await controller
.disconnect()
162 def _get_vca_data(self
) -> Dict
[str, str]:
163 loop
= asyncio
.get_event_loop()
164 data_from_config
= self
._get
_vca
_data
_from
_config
()
165 coro_data_from_controller
= loop
.run_until_complete(self
._get
_vca
_data
_from
_controller
())
166 vca_data
= {**data_from_config
, **coro_data_from_controller
}
167 logger
.debug(f
"vca data={vca_data}")
170 def _get_vca_data_from_config(self
) -> Dict
[str, str]:
171 data
= {"public-key": self
.config
["public-key"]}
172 if self
.config
.get("lxd-cloud"):
173 lxd_cloud_parts
= self
.config
["lxd-cloud"].split(":")
176 "lxd-cloud": lxd_cloud_parts
[0],
177 "lxd-credentials": lxd_cloud_parts
[1]
178 if len(lxd_cloud_parts
) > 1
179 else lxd_cloud_parts
[0],
182 if self
.config
.get("k8s-cloud"):
183 k8s_cloud_parts
= self
.config
["k8s-cloud"].split(":")
186 "k8s-cloud": k8s_cloud_parts
[0],
187 "k8s-credentials": k8s_cloud_parts
[1]
188 if len(k8s_cloud_parts
) > 1
189 else k8s_cloud_parts
[0],
192 if self
.config
.get("model-configs"):
193 data
["model-configs"] = yaml
.safe_load(self
.config
["model-configs"])
197 async def _get_vca_data_from_controller(self
) -> Dict
[str, str]:
198 controller
= Controller()
199 await controller
.connect()
201 connection
= controller
._connector
._connection
203 "endpoints": ",".join(await controller
.api_endpoints
),
204 "user": connection
.username
,
205 "secret": connection
.password
,
206 "cacert": base64
.b64encode(connection
.cacert
.encode("utf-8")).decode("utf-8"),
209 await controller
.disconnect()
212 if __name__
== "__main__": # pragma: no cover
213 main(VcaIntegratorCharm
)