1 |
|
## |
2 |
|
# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. |
3 |
|
# This file is part of OSM |
4 |
|
# All Rights Reserved. |
5 |
|
# |
6 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
7 |
|
# you may not use this file except in compliance with the License. |
8 |
|
# You may obtain a copy of the License at |
9 |
|
# |
10 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
11 |
|
# |
12 |
|
# Unless required by applicable law or agreed to in writing, software |
13 |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
14 |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
15 |
|
# implied. |
16 |
|
# See the License for the specific language governing permissions and |
17 |
|
# limitations under the License. |
18 |
|
# |
19 |
|
# For those usages not covered by the Apache License, Version 2.0 please |
20 |
|
# contact with: nfvlabs@tid.es |
21 |
|
## |
22 |
|
|
23 |
1 |
import asyncio |
24 |
1 |
import logging |
25 |
|
|
26 |
1 |
from n2vc.config import EnvironConfig |
27 |
1 |
from n2vc.definitions import RelationEndpoint |
28 |
1 |
from n2vc.exceptions import ( |
29 |
|
N2VCBadArgumentsException, |
30 |
|
N2VCException, |
31 |
|
N2VCConnectionException, |
32 |
|
N2VCExecutionException, |
33 |
|
N2VCApplicationExists, |
34 |
|
JujuApplicationExists, |
35 |
|
# N2VCNotFound, |
36 |
|
MethodNotImplemented, |
37 |
|
) |
38 |
1 |
from n2vc.n2vc_conn import N2VCConnector |
39 |
1 |
from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml |
40 |
1 |
from n2vc.libjuju import Libjuju, retry_callback |
41 |
1 |
from n2vc.store import MotorStore |
42 |
1 |
from n2vc.utils import get_ee_id_components, generate_random_alfanum_string |
43 |
1 |
from n2vc.vca.connection import get_connection |
44 |
1 |
from retrying_async import retry |
45 |
1 |
from typing import Tuple |
46 |
|
|
47 |
|
|
48 |
1 |
class N2VCJujuConnector(N2VCConnector): |
49 |
|
|
50 |
|
""" |
51 |
|
#################################################################################### |
52 |
|
################################### P U B L I C #################################### |
53 |
|
#################################################################################### |
54 |
|
""" |
55 |
|
|
56 |
1 |
BUILT_IN_CLOUDS = ["localhost", "microk8s"] |
57 |
1 |
libjuju = None |
58 |
|
|
59 |
1 |
def __init__( |
60 |
|
self, |
61 |
|
db: object, |
62 |
|
fs: object, |
63 |
|
log: object = None, |
64 |
|
on_update_db=None, |
65 |
|
): |
66 |
|
""" |
67 |
|
Constructor |
68 |
|
|
69 |
|
:param: db: Database object from osm_common |
70 |
|
:param: fs: Filesystem object from osm_common |
71 |
|
:param: log: Logger |
72 |
|
:param: on_update_db: Callback function to be called for updating the database. |
73 |
|
""" |
74 |
|
|
75 |
|
# parent class constructor |
76 |
1 |
N2VCConnector.__init__(self, db=db, fs=fs, log=log, on_update_db=on_update_db) |
77 |
|
|
78 |
|
# silence websocket traffic log |
79 |
1 |
logging.getLogger("websockets.protocol").setLevel(logging.INFO) |
80 |
1 |
logging.getLogger("juju.client.connection").setLevel(logging.WARN) |
81 |
1 |
logging.getLogger("model").setLevel(logging.WARN) |
82 |
|
|
83 |
1 |
self.log.info("Initializing N2VC juju connector...") |
84 |
|
|
85 |
1 |
db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri") |
86 |
1 |
self._store = MotorStore(db_uri) |
87 |
1 |
self.loading_libjuju = asyncio.Lock() |
88 |
1 |
self.delete_namespace_locks = {} |
89 |
1 |
self.log.info("N2VC juju connector initialized") |
90 |
|
|
91 |
1 |
async def get_status( |
92 |
|
self, namespace: str, yaml_format: bool = True, vca_id: str = None |
93 |
|
): |
94 |
|
""" |
95 |
|
Get status from all juju models from a VCA |
96 |
|
|
97 |
|
:param namespace: we obtain ns from namespace |
98 |
|
:param yaml_format: returns a yaml string |
99 |
|
:param: vca_id: VCA ID from which the status will be retrieved. |
100 |
|
""" |
101 |
|
# TODO: Review where is this function used. It is not optimal at all to get the status |
102 |
|
# from all the juju models of a particular VCA. Additionally, these models might |
103 |
|
# not have been deployed by OSM, in that case we are getting information from |
104 |
|
# deployments outside of OSM's scope. |
105 |
|
|
106 |
|
# self.log.info('Getting NS status. namespace: {}'.format(namespace)) |
107 |
0 |
libjuju = await self._get_libjuju(vca_id) |
108 |
|
|
109 |
0 |
_nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( |
110 |
|
namespace=namespace |
111 |
|
) |
112 |
|
# model name is ns_id |
113 |
0 |
model_name = ns_id |
114 |
0 |
if model_name is None: |
115 |
0 |
msg = "Namespace {} not valid".format(namespace) |
116 |
0 |
self.log.error(msg) |
117 |
0 |
raise N2VCBadArgumentsException(msg, ["namespace"]) |
118 |
|
|
119 |
0 |
status = {} |
120 |
0 |
models = await libjuju.list_models(contains=ns_id) |
121 |
|
|
122 |
0 |
for m in models: |
123 |
0 |
status[m] = await libjuju.get_model_status(m) |
124 |
|
|
125 |
0 |
if yaml_format: |
126 |
0 |
return obj_to_yaml(status) |
127 |
|
else: |
128 |
0 |
return obj_to_dict(status) |
129 |
|
|
130 |
1 |
async def update_vca_status(self, vcastatus: dict, vca_id: str = None): |
131 |
|
""" |
132 |
|
Add all configs, actions, executed actions of all applications in a model to vcastatus dict. |
133 |
|
|
134 |
|
:param vcastatus: dict containing vcaStatus |
135 |
|
:param: vca_id: VCA ID |
136 |
|
|
137 |
|
:return: None |
138 |
|
""" |
139 |
1 |
try: |
140 |
1 |
libjuju = await self._get_libjuju(vca_id) |
141 |
1 |
for model_name in vcastatus: |
142 |
|
# Adding executed actions |
143 |
1 |
vcastatus[model_name][ |
144 |
|
"executedActions" |
145 |
|
] = await libjuju.get_executed_actions(model_name) |
146 |
1 |
for application in vcastatus[model_name]["applications"]: |
147 |
|
# Adding application actions |
148 |
1 |
vcastatus[model_name]["applications"][application][ |
149 |
|
"actions" |
150 |
|
] = await libjuju.get_actions(application, model_name) |
151 |
|
# Adding application configs |
152 |
1 |
vcastatus[model_name]["applications"][application][ |
153 |
|
"configs" |
154 |
|
] = await libjuju.get_application_configs(model_name, application) |
155 |
1 |
except Exception as e: |
156 |
1 |
self.log.debug("Error in updating vca status: {}".format(str(e))) |
157 |
|
|
158 |
1 |
async def create_execution_environment( |
159 |
|
self, |
160 |
|
namespace: str, |
161 |
|
db_dict: dict, |
162 |
|
reuse_ee_id: str = None, |
163 |
|
progress_timeout: float = None, |
164 |
|
total_timeout: float = None, |
165 |
|
vca_id: str = None, |
166 |
|
) -> (str, dict): |
167 |
|
""" |
168 |
|
Create an Execution Environment. Returns when it is created or raises an |
169 |
|
exception on failing |
170 |
|
|
171 |
|
:param: namespace: Contains a dot separate string. |
172 |
|
LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>] |
173 |
|
:param: db_dict: where to write to database when the status changes. |
174 |
|
It contains a dictionary with {collection: str, filter: {}, path: str}, |
175 |
|
e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: |
176 |
|
"_admin.deployed.VCA.3"} |
177 |
|
:param: reuse_ee_id: ee id from an older execution. It allows us to reuse an |
178 |
|
older environment |
179 |
|
:param: progress_timeout: Progress timeout |
180 |
|
:param: total_timeout: Total timeout |
181 |
|
:param: vca_id: VCA ID |
182 |
|
|
183 |
|
:returns: id of the new execution environment and credentials for it |
184 |
|
(credentials can contains hostname, username, etc depending on underlying cloud) |
185 |
|
""" |
186 |
|
|
187 |
0 |
self.log.info( |
188 |
|
"Creating execution environment. namespace: {}, reuse_ee_id: {}".format( |
189 |
|
namespace, reuse_ee_id |
190 |
|
) |
191 |
|
) |
192 |
0 |
libjuju = await self._get_libjuju(vca_id) |
193 |
|
|
194 |
0 |
machine_id = None |
195 |
0 |
if reuse_ee_id: |
196 |
0 |
model_name, application_name, machine_id = self._get_ee_id_components( |
197 |
|
ee_id=reuse_ee_id |
198 |
|
) |
199 |
|
else: |
200 |
0 |
( |
201 |
|
_nsi_id, |
202 |
|
ns_id, |
203 |
|
_vnf_id, |
204 |
|
_vdu_id, |
205 |
|
_vdu_count, |
206 |
|
) = self._get_namespace_components(namespace=namespace) |
207 |
|
# model name is ns_id |
208 |
0 |
model_name = ns_id |
209 |
|
# application name |
210 |
0 |
application_name = self._get_application_name(namespace=namespace) |
211 |
|
|
212 |
0 |
self.log.debug( |
213 |
|
"model name: {}, application name: {}, machine_id: {}".format( |
214 |
|
model_name, application_name, machine_id |
215 |
|
) |
216 |
|
) |
217 |
|
|
218 |
|
# create or reuse a new juju machine |
219 |
0 |
try: |
220 |
0 |
if not await libjuju.model_exists(model_name): |
221 |
0 |
await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud) |
222 |
0 |
machine, new = await libjuju.create_machine( |
223 |
|
model_name=model_name, |
224 |
|
machine_id=machine_id, |
225 |
|
db_dict=db_dict, |
226 |
|
progress_timeout=progress_timeout, |
227 |
|
total_timeout=total_timeout, |
228 |
|
) |
229 |
|
# id for the execution environment |
230 |
0 |
ee_id = N2VCJujuConnector._build_ee_id( |
231 |
|
model_name=model_name, |
232 |
|
application_name=application_name, |
233 |
|
machine_id=str(machine.entity_id), |
234 |
|
) |
235 |
0 |
self.log.debug("ee_id: {}".format(ee_id)) |
236 |
|
|
237 |
0 |
if new: |
238 |
|
# write ee_id in database |
239 |
0 |
self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) |
240 |
|
|
241 |
0 |
except Exception as e: |
242 |
0 |
message = "Error creating machine on juju: {}".format(e) |
243 |
0 |
self.log.error(message) |
244 |
0 |
raise N2VCException(message=message) |
245 |
|
|
246 |
|
# new machine credentials |
247 |
0 |
credentials = {"hostname": machine.dns_name} |
248 |
|
|
249 |
0 |
self.log.info( |
250 |
|
"Execution environment created. ee_id: {}, credentials: {}".format( |
251 |
|
ee_id, credentials |
252 |
|
) |
253 |
|
) |
254 |
|
|
255 |
0 |
return ee_id, credentials |
256 |
|
|
257 |
1 |
async def register_execution_environment( |
258 |
|
self, |
259 |
|
namespace: str, |
260 |
|
credentials: dict, |
261 |
|
db_dict: dict, |
262 |
|
progress_timeout: float = None, |
263 |
|
total_timeout: float = None, |
264 |
|
vca_id: str = None, |
265 |
|
) -> str: |
266 |
|
""" |
267 |
|
Register an existing execution environment at the VCA |
268 |
|
|
269 |
|
:param: namespace: Contains a dot separate string. |
270 |
|
LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>] |
271 |
|
:param: credentials: credentials to access the existing execution environment |
272 |
|
(it can contains hostname, username, path to private key, |
273 |
|
etc depending on underlying cloud) |
274 |
|
:param: db_dict: where to write to database when the status changes. |
275 |
|
It contains a dictionary with {collection: str, filter: {}, path: str}, |
276 |
|
e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: |
277 |
|
"_admin.deployed.VCA.3"} |
278 |
|
:param: reuse_ee_id: ee id from an older execution. It allows us to reuse an |
279 |
|
older environment |
280 |
|
:param: progress_timeout: Progress timeout |
281 |
|
:param: total_timeout: Total timeout |
282 |
|
:param: vca_id: VCA ID |
283 |
|
|
284 |
|
:returns: id of the execution environment |
285 |
|
""" |
286 |
0 |
self.log.info( |
287 |
|
"Registering execution environment. namespace={}, credentials={}".format( |
288 |
|
namespace, credentials |
289 |
|
) |
290 |
|
) |
291 |
0 |
libjuju = await self._get_libjuju(vca_id) |
292 |
|
|
293 |
0 |
if credentials is None: |
294 |
0 |
raise N2VCBadArgumentsException( |
295 |
|
message="credentials are mandatory", bad_args=["credentials"] |
296 |
|
) |
297 |
0 |
if credentials.get("hostname"): |
298 |
0 |
hostname = credentials["hostname"] |
299 |
|
else: |
300 |
0 |
raise N2VCBadArgumentsException( |
301 |
|
message="hostname is mandatory", bad_args=["credentials.hostname"] |
302 |
|
) |
303 |
0 |
if credentials.get("username"): |
304 |
0 |
username = credentials["username"] |
305 |
|
else: |
306 |
0 |
raise N2VCBadArgumentsException( |
307 |
|
message="username is mandatory", bad_args=["credentials.username"] |
308 |
|
) |
309 |
0 |
if "private_key_path" in credentials: |
310 |
0 |
private_key_path = credentials["private_key_path"] |
311 |
|
else: |
312 |
|
# if not passed as argument, use generated private key path |
313 |
0 |
private_key_path = self.private_key_path |
314 |
|
|
315 |
0 |
_nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( |
316 |
|
namespace=namespace |
317 |
|
) |
318 |
|
|
319 |
|
# model name |
320 |
0 |
model_name = ns_id |
321 |
|
# application name |
322 |
0 |
application_name = self._get_application_name(namespace=namespace) |
323 |
|
|
324 |
|
# register machine on juju |
325 |
0 |
try: |
326 |
0 |
if not await libjuju.model_exists(model_name): |
327 |
0 |
await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud) |
328 |
0 |
machine_id = await libjuju.provision_machine( |
329 |
|
model_name=model_name, |
330 |
|
hostname=hostname, |
331 |
|
username=username, |
332 |
|
private_key_path=private_key_path, |
333 |
|
db_dict=db_dict, |
334 |
|
progress_timeout=progress_timeout, |
335 |
|
total_timeout=total_timeout, |
336 |
|
) |
337 |
0 |
except Exception as e: |
338 |
0 |
self.log.error("Error registering machine: {}".format(e)) |
339 |
0 |
raise N2VCException( |
340 |
|
message="Error registering machine on juju: {}".format(e) |
341 |
|
) |
342 |
|
|
343 |
0 |
self.log.info("Machine registered: {}".format(machine_id)) |
344 |
|
|
345 |
|
# id for the execution environment |
346 |
0 |
ee_id = N2VCJujuConnector._build_ee_id( |
347 |
|
model_name=model_name, |
348 |
|
application_name=application_name, |
349 |
|
machine_id=str(machine_id), |
350 |
|
) |
351 |
|
|
352 |
0 |
self.log.info("Execution environment registered. ee_id: {}".format(ee_id)) |
353 |
|
|
354 |
0 |
return ee_id |
355 |
|
|
356 |
|
# In case of native_charm is being deployed, if JujuApplicationExists error happens |
357 |
|
# it will try to add_unit |
358 |
1 |
@retry( |
359 |
|
attempts=3, |
360 |
|
delay=5, |
361 |
|
retry_exceptions=(N2VCApplicationExists,), |
362 |
|
timeout=None, |
363 |
|
callback=retry_callback, |
364 |
|
) |
365 |
1 |
async def install_configuration_sw( |
366 |
|
self, |
367 |
|
ee_id: str, |
368 |
|
artifact_path: str, |
369 |
|
db_dict: dict, |
370 |
|
progress_timeout: float = None, |
371 |
|
total_timeout: float = None, |
372 |
|
config: dict = None, |
373 |
|
num_units: int = 1, |
374 |
|
vca_id: str = None, |
375 |
|
scaling_out: bool = False, |
376 |
|
vca_type: str = None, |
377 |
|
): |
378 |
|
""" |
379 |
|
Install the software inside the execution environment identified by ee_id |
380 |
|
|
381 |
|
:param: ee_id: the id of the execution environment returned by |
382 |
|
create_execution_environment or register_execution_environment |
383 |
|
:param: artifact_path: where to locate the artifacts (parent folder) using |
384 |
|
the self.fs |
385 |
|
the final artifact path will be a combination of this |
386 |
|
artifact_path and additional string from the config_dict |
387 |
|
(e.g. charm name) |
388 |
|
:param: db_dict: where to write into database when the status changes. |
389 |
|
It contains a dict with |
390 |
|
{collection: <str>, filter: {}, path: <str>}, |
391 |
|
e.g. {collection: "nsrs", filter: |
392 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
393 |
|
:param: progress_timeout: Progress timeout |
394 |
|
:param: total_timeout: Total timeout |
395 |
|
:param: config: Dictionary with deployment config information. |
396 |
|
:param: num_units: Number of units to deploy of a particular charm. |
397 |
|
:param: vca_id: VCA ID |
398 |
|
:param: scaling_out: Boolean to indicate if it is a scaling out operation |
399 |
|
:param: vca_type: VCA type |
400 |
|
""" |
401 |
|
|
402 |
0 |
self.log.info( |
403 |
|
( |
404 |
|
"Installing configuration sw on ee_id: {}, " |
405 |
|
"artifact path: {}, db_dict: {}" |
406 |
|
).format(ee_id, artifact_path, db_dict) |
407 |
|
) |
408 |
0 |
libjuju = await self._get_libjuju(vca_id) |
409 |
|
|
410 |
|
# check arguments |
411 |
0 |
if ee_id is None or len(ee_id) == 0: |
412 |
0 |
raise N2VCBadArgumentsException( |
413 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
414 |
|
) |
415 |
0 |
if artifact_path is None or len(artifact_path) == 0: |
416 |
0 |
raise N2VCBadArgumentsException( |
417 |
|
message="artifact_path is mandatory", bad_args=["artifact_path"] |
418 |
|
) |
419 |
0 |
if db_dict is None: |
420 |
0 |
raise N2VCBadArgumentsException( |
421 |
|
message="db_dict is mandatory", bad_args=["db_dict"] |
422 |
|
) |
423 |
|
|
424 |
0 |
try: |
425 |
0 |
( |
426 |
|
model_name, |
427 |
|
application_name, |
428 |
|
machine_id, |
429 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
430 |
0 |
self.log.debug( |
431 |
|
"model: {}, application: {}, machine: {}".format( |
432 |
|
model_name, application_name, machine_id |
433 |
|
) |
434 |
|
) |
435 |
0 |
except Exception: |
436 |
0 |
raise N2VCBadArgumentsException( |
437 |
|
message="ee_id={} is not a valid execution environment id".format( |
438 |
|
ee_id |
439 |
|
), |
440 |
|
bad_args=["ee_id"], |
441 |
|
) |
442 |
|
|
443 |
|
# remove // in charm path |
444 |
0 |
while artifact_path.find("//") >= 0: |
445 |
0 |
artifact_path = artifact_path.replace("//", "/") |
446 |
|
|
447 |
|
# check charm path |
448 |
0 |
if not self.fs.file_exists(artifact_path): |
449 |
0 |
msg = "artifact path does not exist: {}".format(artifact_path) |
450 |
0 |
raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"]) |
451 |
|
|
452 |
0 |
if artifact_path.startswith("/"): |
453 |
0 |
full_path = self.fs.path + artifact_path |
454 |
|
else: |
455 |
0 |
full_path = self.fs.path + "/" + artifact_path |
456 |
|
|
457 |
0 |
try: |
458 |
0 |
if vca_type == "native_charm" and await libjuju.check_application_exists( |
459 |
|
model_name, application_name |
460 |
|
): |
461 |
0 |
await libjuju.add_unit( |
462 |
|
application_name=application_name, |
463 |
|
model_name=model_name, |
464 |
|
machine_id=machine_id, |
465 |
|
db_dict=db_dict, |
466 |
|
progress_timeout=progress_timeout, |
467 |
|
total_timeout=total_timeout, |
468 |
|
) |
469 |
|
else: |
470 |
0 |
await libjuju.deploy_charm( |
471 |
|
model_name=model_name, |
472 |
|
application_name=application_name, |
473 |
|
path=full_path, |
474 |
|
machine_id=machine_id, |
475 |
|
db_dict=db_dict, |
476 |
|
progress_timeout=progress_timeout, |
477 |
|
total_timeout=total_timeout, |
478 |
|
config=config, |
479 |
|
num_units=num_units, |
480 |
|
) |
481 |
0 |
except JujuApplicationExists as e: |
482 |
0 |
raise N2VCApplicationExists( |
483 |
|
message="Error deploying charm into ee={} : {}".format(ee_id, e.message) |
484 |
|
) |
485 |
0 |
except Exception as e: |
486 |
0 |
raise N2VCException( |
487 |
|
message="Error deploying charm into ee={} : {}".format(ee_id, e) |
488 |
|
) |
489 |
|
|
490 |
0 |
self.log.info("Configuration sw installed") |
491 |
|
|
492 |
1 |
async def install_k8s_proxy_charm( |
493 |
|
self, |
494 |
|
charm_name: str, |
495 |
|
namespace: str, |
496 |
|
artifact_path: str, |
497 |
|
db_dict: dict, |
498 |
|
progress_timeout: float = None, |
499 |
|
total_timeout: float = None, |
500 |
|
config: dict = None, |
501 |
|
vca_id: str = None, |
502 |
|
) -> str: |
503 |
|
""" |
504 |
|
Install a k8s proxy charm |
505 |
|
|
506 |
|
:param charm_name: Name of the charm being deployed |
507 |
|
:param namespace: collection of all the uuids related to the charm. |
508 |
|
:param str artifact_path: where to locate the artifacts (parent folder) using |
509 |
|
the self.fs |
510 |
|
the final artifact path will be a combination of this artifact_path and |
511 |
|
additional string from the config_dict (e.g. charm name) |
512 |
|
:param dict db_dict: where to write into database when the status changes. |
513 |
|
It contains a dict with |
514 |
|
{collection: <str>, filter: {}, path: <str>}, |
515 |
|
e.g. {collection: "nsrs", filter: |
516 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
517 |
|
:param: progress_timeout: Progress timeout |
518 |
|
:param: total_timeout: Total timeout |
519 |
|
:param config: Dictionary with additional configuration |
520 |
|
:param vca_id: VCA ID |
521 |
|
|
522 |
|
:returns ee_id: execution environment id. |
523 |
|
""" |
524 |
1 |
self.log.info( |
525 |
|
"Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format( |
526 |
|
charm_name, artifact_path, db_dict |
527 |
|
) |
528 |
|
) |
529 |
1 |
libjuju = await self._get_libjuju(vca_id) |
530 |
|
|
531 |
1 |
if artifact_path is None or len(artifact_path) == 0: |
532 |
1 |
raise N2VCBadArgumentsException( |
533 |
|
message="artifact_path is mandatory", bad_args=["artifact_path"] |
534 |
|
) |
535 |
1 |
if db_dict is None: |
536 |
1 |
raise N2VCBadArgumentsException( |
537 |
|
message="db_dict is mandatory", bad_args=["db_dict"] |
538 |
|
) |
539 |
|
|
540 |
|
# remove // in charm path |
541 |
1 |
while artifact_path.find("//") >= 0: |
542 |
0 |
artifact_path = artifact_path.replace("//", "/") |
543 |
|
|
544 |
|
# check charm path |
545 |
1 |
if not self.fs.file_exists(artifact_path): |
546 |
1 |
msg = "artifact path does not exist: {}".format(artifact_path) |
547 |
1 |
raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"]) |
548 |
|
|
549 |
1 |
if artifact_path.startswith("/"): |
550 |
0 |
full_path = self.fs.path + artifact_path |
551 |
|
else: |
552 |
1 |
full_path = self.fs.path + "/" + artifact_path |
553 |
|
|
554 |
1 |
_, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace) |
555 |
1 |
model_name = "{}-k8s".format(ns_id) |
556 |
1 |
if not await libjuju.model_exists(model_name): |
557 |
1 |
await libjuju.add_model(model_name, libjuju.vca_connection.k8s_cloud) |
558 |
1 |
application_name = self._get_application_name(namespace) |
559 |
|
|
560 |
1 |
try: |
561 |
1 |
await libjuju.deploy_charm( |
562 |
|
model_name=model_name, |
563 |
|
application_name=application_name, |
564 |
|
path=full_path, |
565 |
|
machine_id=None, |
566 |
|
db_dict=db_dict, |
567 |
|
progress_timeout=progress_timeout, |
568 |
|
total_timeout=total_timeout, |
569 |
|
config=config, |
570 |
|
) |
571 |
1 |
except Exception as e: |
572 |
1 |
raise N2VCException(message="Error deploying charm: {}".format(e)) |
573 |
|
|
574 |
1 |
self.log.info("K8s proxy charm installed") |
575 |
1 |
ee_id = N2VCJujuConnector._build_ee_id( |
576 |
|
model_name=model_name, application_name=application_name, machine_id="k8s" |
577 |
|
) |
578 |
|
|
579 |
1 |
self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) |
580 |
|
|
581 |
1 |
return ee_id |
582 |
|
|
583 |
1 |
async def get_ee_ssh_public__key( |
584 |
|
self, |
585 |
|
ee_id: str, |
586 |
|
db_dict: dict, |
587 |
|
progress_timeout: float = None, |
588 |
|
total_timeout: float = None, |
589 |
|
vca_id: str = None, |
590 |
|
) -> str: |
591 |
|
""" |
592 |
|
Get Execution environment ssh public key |
593 |
|
|
594 |
|
:param: ee_id: the id of the execution environment returned by |
595 |
|
create_execution_environment or register_execution_environment |
596 |
|
:param: db_dict: where to write into database when the status changes. |
597 |
|
It contains a dict with |
598 |
|
{collection: <str>, filter: {}, path: <str>}, |
599 |
|
e.g. {collection: "nsrs", filter: |
600 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
601 |
|
:param: progress_timeout: Progress timeout |
602 |
|
:param: total_timeout: Total timeout |
603 |
|
:param vca_id: VCA ID |
604 |
|
:returns: public key of the execution environment |
605 |
|
For the case of juju proxy charm ssh-layered, it is the one |
606 |
|
returned by 'get-ssh-public-key' primitive. |
607 |
|
It raises a N2VC exception if fails |
608 |
|
""" |
609 |
|
|
610 |
0 |
self.log.info( |
611 |
|
( |
612 |
|
"Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}" |
613 |
|
).format(ee_id, db_dict) |
614 |
|
) |
615 |
0 |
libjuju = await self._get_libjuju(vca_id) |
616 |
|
|
617 |
|
# check arguments |
618 |
0 |
if ee_id is None or len(ee_id) == 0: |
619 |
0 |
raise N2VCBadArgumentsException( |
620 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
621 |
|
) |
622 |
0 |
if db_dict is None: |
623 |
0 |
raise N2VCBadArgumentsException( |
624 |
|
message="db_dict is mandatory", bad_args=["db_dict"] |
625 |
|
) |
626 |
|
|
627 |
0 |
try: |
628 |
0 |
( |
629 |
|
model_name, |
630 |
|
application_name, |
631 |
|
machine_id, |
632 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
633 |
0 |
self.log.debug( |
634 |
|
"model: {}, application: {}, machine: {}".format( |
635 |
|
model_name, application_name, machine_id |
636 |
|
) |
637 |
|
) |
638 |
0 |
except Exception: |
639 |
0 |
raise N2VCBadArgumentsException( |
640 |
|
message="ee_id={} is not a valid execution environment id".format( |
641 |
|
ee_id |
642 |
|
), |
643 |
|
bad_args=["ee_id"], |
644 |
|
) |
645 |
|
|
646 |
|
# try to execute ssh layer primitives (if exist): |
647 |
|
# generate-ssh-key |
648 |
|
# get-ssh-public-key |
649 |
|
|
650 |
0 |
output = None |
651 |
|
|
652 |
0 |
application_name = N2VCJujuConnector._format_app_name(application_name) |
653 |
|
|
654 |
|
# execute action: generate-ssh-key |
655 |
0 |
try: |
656 |
0 |
output, _status = await libjuju.execute_action( |
657 |
|
model_name=model_name, |
658 |
|
application_name=application_name, |
659 |
|
action_name="generate-ssh-key", |
660 |
|
db_dict=db_dict, |
661 |
|
progress_timeout=progress_timeout, |
662 |
|
total_timeout=total_timeout, |
663 |
|
) |
664 |
0 |
except Exception as e: |
665 |
0 |
self.log.info( |
666 |
|
"Skipping exception while executing action generate-ssh-key: {}".format( |
667 |
|
e |
668 |
|
) |
669 |
|
) |
670 |
|
|
671 |
|
# execute action: get-ssh-public-key |
672 |
0 |
try: |
673 |
0 |
output, _status = await libjuju.execute_action( |
674 |
|
model_name=model_name, |
675 |
|
application_name=application_name, |
676 |
|
action_name="get-ssh-public-key", |
677 |
|
db_dict=db_dict, |
678 |
|
progress_timeout=progress_timeout, |
679 |
|
total_timeout=total_timeout, |
680 |
|
) |
681 |
0 |
except Exception as e: |
682 |
0 |
msg = "Cannot execute action get-ssh-public-key: {}\n".format(e) |
683 |
0 |
self.log.info(msg) |
684 |
0 |
raise N2VCExecutionException(e, primitive_name="get-ssh-public-key") |
685 |
|
|
686 |
|
# return public key if exists |
687 |
0 |
return output["pubkey"] if "pubkey" in output else output |
688 |
|
|
689 |
1 |
async def get_metrics( |
690 |
|
self, model_name: str, application_name: str, vca_id: str = None |
691 |
|
) -> dict: |
692 |
|
""" |
693 |
|
Get metrics from application |
694 |
|
|
695 |
|
:param: model_name: Model name |
696 |
|
:param: application_name: Application name |
697 |
|
:param: vca_id: VCA ID |
698 |
|
|
699 |
|
:return: Dictionary with obtained metrics |
700 |
|
""" |
701 |
1 |
libjuju = await self._get_libjuju(vca_id) |
702 |
1 |
return await libjuju.get_metrics(model_name, application_name) |
703 |
|
|
704 |
1 |
async def add_relation( |
705 |
|
self, provider: RelationEndpoint, requirer: RelationEndpoint |
706 |
|
): |
707 |
|
""" |
708 |
|
Add relation between two charmed endpoints |
709 |
|
|
710 |
|
:param: provider: Provider relation endpoint |
711 |
|
:param: requirer: Requirer relation endpoint |
712 |
|
""" |
713 |
1 |
self.log.debug(f"adding new relation between {provider} and {requirer}") |
714 |
1 |
cross_model_relation = ( |
715 |
|
provider.model_name != requirer.model_name |
716 |
|
or provider.vca_id != requirer.vca_id |
717 |
|
) |
718 |
1 |
try: |
719 |
1 |
if cross_model_relation: |
720 |
|
# Cross-model relation |
721 |
1 |
provider_libjuju = await self._get_libjuju(provider.vca_id) |
722 |
1 |
requirer_libjuju = await self._get_libjuju(requirer.vca_id) |
723 |
1 |
offer = await provider_libjuju.offer(provider) |
724 |
1 |
if offer: |
725 |
1 |
saas_name = await requirer_libjuju.consume( |
726 |
|
requirer.model_name, offer, provider_libjuju |
727 |
|
) |
728 |
1 |
await requirer_libjuju.add_relation( |
729 |
|
requirer.model_name, requirer.endpoint, saas_name |
730 |
|
) |
731 |
|
else: |
732 |
|
# Standard relation |
733 |
1 |
vca_id = provider.vca_id |
734 |
1 |
model = provider.model_name |
735 |
1 |
libjuju = await self._get_libjuju(vca_id) |
736 |
|
# add juju relations between two applications |
737 |
1 |
await libjuju.add_relation( |
738 |
|
model_name=model, |
739 |
|
endpoint_1=provider.endpoint, |
740 |
|
endpoint_2=requirer.endpoint, |
741 |
|
) |
742 |
1 |
except Exception as e: |
743 |
1 |
message = f"Error adding relation between {provider} and {requirer}: {e}" |
744 |
1 |
self.log.error(message) |
745 |
1 |
raise N2VCException(message=message) |
746 |
|
|
747 |
1 |
async def remove_relation(self): |
748 |
|
# TODO |
749 |
0 |
self.log.info("Method not implemented yet") |
750 |
0 |
raise MethodNotImplemented() |
751 |
|
|
752 |
1 |
async def deregister_execution_environments(self): |
753 |
0 |
self.log.info("Method not implemented yet") |
754 |
0 |
raise MethodNotImplemented() |
755 |
|
|
756 |
1 |
async def delete_namespace( |
757 |
|
self, |
758 |
|
namespace: str, |
759 |
|
db_dict: dict = None, |
760 |
|
total_timeout: float = None, |
761 |
|
vca_id: str = None, |
762 |
|
): |
763 |
|
""" |
764 |
|
Remove a network scenario and its execution environments |
765 |
|
:param: namespace: [<nsi-id>].<ns-id> |
766 |
|
:param: db_dict: where to write into database when the status changes. |
767 |
|
It contains a dict with |
768 |
|
{collection: <str>, filter: {}, path: <str>}, |
769 |
|
e.g. {collection: "nsrs", filter: |
770 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
771 |
|
:param: total_timeout: Total timeout |
772 |
|
:param: vca_id: VCA ID |
773 |
|
""" |
774 |
0 |
self.log.info("Deleting namespace={}".format(namespace)) |
775 |
0 |
will_not_delete = False |
776 |
0 |
if namespace not in self.delete_namespace_locks: |
777 |
0 |
self.delete_namespace_locks[namespace] = asyncio.Lock() |
778 |
0 |
delete_lock = self.delete_namespace_locks[namespace] |
779 |
|
|
780 |
0 |
while delete_lock.locked(): |
781 |
0 |
will_not_delete = True |
782 |
0 |
await asyncio.sleep(0.1) |
783 |
|
|
784 |
0 |
if will_not_delete: |
785 |
0 |
self.log.info("Namespace {} deleted by another worker.".format(namespace)) |
786 |
0 |
return |
787 |
|
|
788 |
0 |
try: |
789 |
0 |
async with delete_lock: |
790 |
0 |
libjuju = await self._get_libjuju(vca_id) |
791 |
|
|
792 |
|
# check arguments |
793 |
0 |
if namespace is None: |
794 |
0 |
raise N2VCBadArgumentsException( |
795 |
|
message="namespace is mandatory", bad_args=["namespace"] |
796 |
|
) |
797 |
|
|
798 |
0 |
( |
799 |
|
_nsi_id, |
800 |
|
ns_id, |
801 |
|
_vnf_id, |
802 |
|
_vdu_id, |
803 |
|
_vdu_count, |
804 |
|
) = self._get_namespace_components(namespace=namespace) |
805 |
0 |
if ns_id is not None: |
806 |
0 |
try: |
807 |
0 |
models = await libjuju.list_models(contains=ns_id) |
808 |
0 |
for model in models: |
809 |
0 |
await libjuju.destroy_model( |
810 |
|
model_name=model, total_timeout=total_timeout |
811 |
|
) |
812 |
0 |
except Exception as e: |
813 |
0 |
self.log.error(f"Error deleting namespace {namespace} : {e}") |
814 |
0 |
raise N2VCException( |
815 |
|
message="Error deleting namespace {} : {}".format( |
816 |
|
namespace, e |
817 |
|
) |
818 |
|
) |
819 |
|
else: |
820 |
0 |
raise N2VCBadArgumentsException( |
821 |
|
message="only ns_id is permitted to delete yet", |
822 |
|
bad_args=["namespace"], |
823 |
|
) |
824 |
0 |
except Exception as e: |
825 |
0 |
self.log.error(f"Error deleting namespace {namespace} : {e}") |
826 |
0 |
raise e |
827 |
|
finally: |
828 |
0 |
self.delete_namespace_locks.pop(namespace) |
829 |
0 |
self.log.info("Namespace {} deleted".format(namespace)) |
830 |
|
|
831 |
1 |
async def delete_execution_environment( |
832 |
|
self, |
833 |
|
ee_id: str, |
834 |
|
db_dict: dict = None, |
835 |
|
total_timeout: float = None, |
836 |
|
scaling_in: bool = False, |
837 |
|
vca_type: str = None, |
838 |
|
vca_id: str = None, |
839 |
|
application_to_delete: str = None, |
840 |
|
): |
841 |
|
""" |
842 |
|
Delete an execution environment |
843 |
|
:param str ee_id: id of the execution environment to delete |
844 |
|
:param dict db_dict: where to write into database when the status changes. |
845 |
|
It contains a dict with |
846 |
|
{collection: <str>, filter: {}, path: <str>}, |
847 |
|
e.g. {collection: "nsrs", filter: |
848 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
849 |
|
:param total_timeout: Total timeout |
850 |
|
:param scaling_in: Boolean to indicate if it is a scaling in operation |
851 |
|
:param vca_type: VCA type |
852 |
|
:param vca_id: VCA ID |
853 |
|
:param application_to_delete: name of the single application to be deleted |
854 |
|
""" |
855 |
1 |
self.log.info("Deleting execution environment ee_id={}".format(ee_id)) |
856 |
1 |
libjuju = await self._get_libjuju(vca_id) |
857 |
|
|
858 |
|
# check arguments |
859 |
1 |
if ee_id is None: |
860 |
0 |
raise N2VCBadArgumentsException( |
861 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
862 |
|
) |
863 |
|
|
864 |
1 |
model_name, application_name, machine_id = self._get_ee_id_components( |
865 |
|
ee_id=ee_id |
866 |
|
) |
867 |
1 |
try: |
868 |
1 |
if application_to_delete == application_name: |
869 |
|
# destroy the application |
870 |
1 |
await libjuju.destroy_application( |
871 |
|
model_name=model_name, |
872 |
|
application_name=application_name, |
873 |
|
total_timeout=total_timeout, |
874 |
|
) |
875 |
|
# if model is empty delete it |
876 |
1 |
controller = await libjuju.get_controller() |
877 |
1 |
model = await libjuju.get_model( |
878 |
|
controller=controller, |
879 |
|
model_name=model_name, |
880 |
|
) |
881 |
1 |
if not model.applications: |
882 |
1 |
self.log.info("Model {} is empty, deleting it".format(model_name)) |
883 |
1 |
await libjuju.destroy_model( |
884 |
|
model_name=model_name, |
885 |
|
total_timeout=total_timeout, |
886 |
|
) |
887 |
1 |
elif not scaling_in: |
888 |
|
# destroy the model |
889 |
1 |
await libjuju.destroy_model( |
890 |
|
model_name=model_name, total_timeout=total_timeout |
891 |
|
) |
892 |
0 |
elif vca_type == "native_charm" and scaling_in: |
893 |
|
# destroy the unit in the application |
894 |
0 |
await libjuju.destroy_unit( |
895 |
|
application_name=application_name, |
896 |
|
model_name=model_name, |
897 |
|
machine_id=machine_id, |
898 |
|
total_timeout=total_timeout, |
899 |
|
) |
900 |
|
else: |
901 |
|
# destroy the application |
902 |
0 |
await libjuju.destroy_application( |
903 |
|
model_name=model_name, |
904 |
|
application_name=application_name, |
905 |
|
total_timeout=total_timeout, |
906 |
|
) |
907 |
0 |
except Exception as e: |
908 |
0 |
raise N2VCException( |
909 |
|
message=( |
910 |
|
"Error deleting execution environment {} (application {}) : {}" |
911 |
|
).format(ee_id, application_name, e) |
912 |
|
) |
913 |
|
|
914 |
1 |
self.log.info("Execution environment {} deleted".format(ee_id)) |
915 |
|
|
916 |
1 |
async def exec_primitive( |
917 |
|
self, |
918 |
|
ee_id: str, |
919 |
|
primitive_name: str, |
920 |
|
params_dict: dict, |
921 |
|
db_dict: dict = None, |
922 |
|
progress_timeout: float = None, |
923 |
|
total_timeout: float = None, |
924 |
|
vca_id: str = None, |
925 |
|
vca_type: str = None, |
926 |
|
) -> str: |
927 |
|
""" |
928 |
|
Execute a primitive in the execution environment |
929 |
|
|
930 |
|
:param: ee_id: the one returned by create_execution_environment or |
931 |
|
register_execution_environment |
932 |
|
:param: primitive_name: must be one defined in the software. There is one |
933 |
|
called 'config', where, for the proxy case, the 'credentials' of VM are |
934 |
|
provided |
935 |
|
:param: params_dict: parameters of the action |
936 |
|
:param: db_dict: where to write into database when the status changes. |
937 |
|
It contains a dict with |
938 |
|
{collection: <str>, filter: {}, path: <str>}, |
939 |
|
e.g. {collection: "nsrs", filter: |
940 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
941 |
|
:param: progress_timeout: Progress timeout |
942 |
|
:param: total_timeout: Total timeout |
943 |
|
:param: vca_id: VCA ID |
944 |
|
:param: vca_type: VCA type |
945 |
|
:returns str: primitive result, if ok. It raises exceptions in case of fail |
946 |
|
""" |
947 |
|
|
948 |
0 |
self.log.info( |
949 |
|
"Executing primitive: {} on ee: {}, params: {}".format( |
950 |
|
primitive_name, ee_id, params_dict |
951 |
|
) |
952 |
|
) |
953 |
0 |
libjuju = await self._get_libjuju(vca_id) |
954 |
|
|
955 |
|
# check arguments |
956 |
0 |
if ee_id is None or len(ee_id) == 0: |
957 |
0 |
raise N2VCBadArgumentsException( |
958 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
959 |
|
) |
960 |
0 |
if primitive_name is None or len(primitive_name) == 0: |
961 |
0 |
raise N2VCBadArgumentsException( |
962 |
|
message="action_name is mandatory", bad_args=["action_name"] |
963 |
|
) |
964 |
0 |
if params_dict is None: |
965 |
0 |
params_dict = dict() |
966 |
|
|
967 |
0 |
try: |
968 |
0 |
( |
969 |
|
model_name, |
970 |
|
application_name, |
971 |
|
machine_id, |
972 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
973 |
|
# To run action on the leader unit in libjuju.execute_action function, |
974 |
|
# machine_id must be set to None if vca_type is not native_charm |
975 |
0 |
if vca_type != "native_charm": |
976 |
0 |
machine_id = None |
977 |
0 |
except Exception: |
978 |
0 |
raise N2VCBadArgumentsException( |
979 |
|
message="ee_id={} is not a valid execution environment id".format( |
980 |
|
ee_id |
981 |
|
), |
982 |
|
bad_args=["ee_id"], |
983 |
|
) |
984 |
|
|
985 |
0 |
if primitive_name == "config": |
986 |
|
# Special case: config primitive |
987 |
0 |
try: |
988 |
0 |
await libjuju.configure_application( |
989 |
|
model_name=model_name, |
990 |
|
application_name=application_name, |
991 |
|
config=params_dict, |
992 |
|
) |
993 |
0 |
actions = await libjuju.get_actions( |
994 |
|
application_name=application_name, model_name=model_name |
995 |
|
) |
996 |
0 |
self.log.debug( |
997 |
|
"Application {} has these actions: {}".format( |
998 |
|
application_name, actions |
999 |
|
) |
1000 |
|
) |
1001 |
0 |
if "verify-ssh-credentials" in actions: |
1002 |
|
# execute verify-credentials |
1003 |
0 |
num_retries = 20 |
1004 |
0 |
retry_timeout = 15.0 |
1005 |
0 |
for _ in range(num_retries): |
1006 |
0 |
try: |
1007 |
0 |
self.log.debug("Executing action verify-ssh-credentials...") |
1008 |
0 |
output, ok = await libjuju.execute_action( |
1009 |
|
model_name=model_name, |
1010 |
|
application_name=application_name, |
1011 |
|
action_name="verify-ssh-credentials", |
1012 |
|
db_dict=db_dict, |
1013 |
|
progress_timeout=progress_timeout, |
1014 |
|
total_timeout=total_timeout, |
1015 |
|
) |
1016 |
|
|
1017 |
0 |
if ok == "failed": |
1018 |
0 |
self.log.debug( |
1019 |
|
"Error executing verify-ssh-credentials: {}. Retrying..." |
1020 |
|
) |
1021 |
0 |
await asyncio.sleep(retry_timeout) |
1022 |
|
|
1023 |
0 |
continue |
1024 |
0 |
self.log.debug("Result: {}, output: {}".format(ok, output)) |
1025 |
0 |
break |
1026 |
0 |
except asyncio.CancelledError: |
1027 |
0 |
raise |
1028 |
|
else: |
1029 |
0 |
self.log.error( |
1030 |
|
"Error executing verify-ssh-credentials after {} retries. ".format( |
1031 |
|
num_retries |
1032 |
|
) |
1033 |
|
) |
1034 |
|
else: |
1035 |
0 |
msg = "Action verify-ssh-credentials does not exist in application {}".format( |
1036 |
|
application_name |
1037 |
|
) |
1038 |
0 |
self.log.debug(msg=msg) |
1039 |
0 |
except Exception as e: |
1040 |
0 |
self.log.error("Error configuring juju application: {}".format(e)) |
1041 |
0 |
raise N2VCExecutionException( |
1042 |
|
message="Error configuring application into ee={} : {}".format( |
1043 |
|
ee_id, e |
1044 |
|
), |
1045 |
|
primitive_name=primitive_name, |
1046 |
|
) |
1047 |
0 |
return "CONFIG OK" |
1048 |
|
else: |
1049 |
0 |
try: |
1050 |
0 |
output, status = await libjuju.execute_action( |
1051 |
|
model_name=model_name, |
1052 |
|
application_name=application_name, |
1053 |
|
action_name=primitive_name, |
1054 |
|
db_dict=db_dict, |
1055 |
|
machine_id=machine_id, |
1056 |
|
progress_timeout=progress_timeout, |
1057 |
|
total_timeout=total_timeout, |
1058 |
|
**params_dict, |
1059 |
|
) |
1060 |
0 |
if status == "completed": |
1061 |
0 |
return output |
1062 |
|
else: |
1063 |
0 |
if "output" in output: |
1064 |
0 |
raise Exception(f'{status}: {output["output"]}') |
1065 |
|
else: |
1066 |
0 |
raise Exception( |
1067 |
|
f"{status}: No further information received from action" |
1068 |
|
) |
1069 |
|
|
1070 |
0 |
except Exception as e: |
1071 |
0 |
self.log.error(f"Error executing primitive {primitive_name}: {e}") |
1072 |
0 |
raise N2VCExecutionException( |
1073 |
|
message=f"Error executing primitive {primitive_name} in ee={ee_id}: {e}", |
1074 |
|
primitive_name=primitive_name, |
1075 |
|
) |
1076 |
|
|
1077 |
1 |
async def upgrade_charm( |
1078 |
|
self, |
1079 |
|
ee_id: str = None, |
1080 |
|
path: str = None, |
1081 |
|
charm_id: str = None, |
1082 |
|
charm_type: str = None, |
1083 |
|
timeout: float = None, |
1084 |
|
) -> str: |
1085 |
|
"""This method upgrade charms in VNFs |
1086 |
|
|
1087 |
|
Args: |
1088 |
|
ee_id: Execution environment id |
1089 |
|
path: Local path to the charm |
1090 |
|
charm_id: charm-id |
1091 |
|
charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm |
1092 |
|
timeout: (Float) Timeout for the ns update operation |
1093 |
|
|
1094 |
|
Returns: |
1095 |
|
The output of the update operation if status equals to "completed" |
1096 |
|
|
1097 |
|
""" |
1098 |
1 |
self.log.info("Upgrading charm: {} on ee: {}".format(path, ee_id)) |
1099 |
1 |
libjuju = await self._get_libjuju(charm_id) |
1100 |
|
|
1101 |
|
# check arguments |
1102 |
1 |
if ee_id is None or len(ee_id) == 0: |
1103 |
1 |
raise N2VCBadArgumentsException( |
1104 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
1105 |
|
) |
1106 |
1 |
try: |
1107 |
1 |
( |
1108 |
|
model_name, |
1109 |
|
application_name, |
1110 |
|
machine_id, |
1111 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
1112 |
|
|
1113 |
1 |
except Exception: |
1114 |
1 |
raise N2VCBadArgumentsException( |
1115 |
|
message="ee_id={} is not a valid execution environment id".format( |
1116 |
|
ee_id |
1117 |
|
), |
1118 |
|
bad_args=["ee_id"], |
1119 |
|
) |
1120 |
|
|
1121 |
1 |
try: |
1122 |
1 |
await libjuju.upgrade_charm( |
1123 |
|
application_name=application_name, |
1124 |
|
path=path, |
1125 |
|
model_name=model_name, |
1126 |
|
total_timeout=timeout, |
1127 |
|
) |
1128 |
|
|
1129 |
1 |
return f"Charm upgraded with application name {application_name}" |
1130 |
|
|
1131 |
1 |
except Exception as e: |
1132 |
1 |
self.log.error("Error upgrading charm {}: {}".format(path, e)) |
1133 |
|
|
1134 |
1 |
raise N2VCException( |
1135 |
|
message="Error upgrading charm {} in ee={} : {}".format(path, ee_id, e) |
1136 |
|
) |
1137 |
|
|
1138 |
1 |
async def disconnect(self, vca_id: str = None): |
1139 |
|
""" |
1140 |
|
Disconnect from VCA |
1141 |
|
|
1142 |
|
:param: vca_id: VCA ID |
1143 |
|
""" |
1144 |
0 |
self.log.info("closing juju N2VC...") |
1145 |
0 |
libjuju = await self._get_libjuju(vca_id) |
1146 |
0 |
try: |
1147 |
0 |
await libjuju.disconnect() |
1148 |
0 |
except Exception as e: |
1149 |
0 |
raise N2VCConnectionException( |
1150 |
|
message="Error disconnecting controller: {}".format(e), |
1151 |
|
url=libjuju.vca_connection.data.endpoints, |
1152 |
|
) |
1153 |
|
|
1154 |
1 |
""" |
1155 |
|
#################################################################################### |
1156 |
|
################################### P R I V A T E ################################## |
1157 |
|
#################################################################################### |
1158 |
|
""" |
1159 |
|
|
1160 |
1 |
async def _get_libjuju(self, vca_id: str = None) -> Libjuju: |
1161 |
|
""" |
1162 |
|
Get libjuju object |
1163 |
|
|
1164 |
|
:param: vca_id: VCA ID |
1165 |
|
If None, get a libjuju object with a Connection to the default VCA |
1166 |
|
Else, geta libjuju object with a Connection to the specified VCA |
1167 |
|
""" |
1168 |
1 |
if not vca_id: |
1169 |
1 |
while self.loading_libjuju.locked(): |
1170 |
0 |
await asyncio.sleep(0.1) |
1171 |
1 |
if not self.libjuju: |
1172 |
0 |
async with self.loading_libjuju: |
1173 |
0 |
vca_connection = await get_connection(self._store) |
1174 |
0 |
self.libjuju = Libjuju(vca_connection, log=self.log) |
1175 |
1 |
return self.libjuju |
1176 |
|
else: |
1177 |
0 |
vca_connection = await get_connection(self._store, vca_id) |
1178 |
0 |
return Libjuju(vca_connection, log=self.log, n2vc=self) |
1179 |
|
|
1180 |
1 |
def _write_ee_id_db(self, db_dict: dict, ee_id: str): |
1181 |
|
# write ee_id to database: _admin.deployed.VCA.x |
1182 |
1 |
try: |
1183 |
1 |
the_table = db_dict["collection"] |
1184 |
0 |
the_filter = db_dict["filter"] |
1185 |
0 |
the_path = db_dict["path"] |
1186 |
0 |
if not the_path[-1] == ".": |
1187 |
0 |
the_path = the_path + "." |
1188 |
0 |
update_dict = {the_path + "ee_id": ee_id} |
1189 |
|
# self.log.debug('Writing ee_id to database: {}'.format(the_path)) |
1190 |
0 |
self.db.set_one( |
1191 |
|
table=the_table, |
1192 |
|
q_filter=the_filter, |
1193 |
|
update_dict=update_dict, |
1194 |
|
fail_on_empty=True, |
1195 |
|
) |
1196 |
1 |
except asyncio.CancelledError: |
1197 |
0 |
raise |
1198 |
1 |
except Exception as e: |
1199 |
1 |
self.log.error("Error writing ee_id to database: {}".format(e)) |
1200 |
|
|
1201 |
1 |
@staticmethod |
1202 |
1 |
def _build_ee_id(model_name: str, application_name: str, machine_id: str): |
1203 |
|
""" |
1204 |
|
Build an execution environment id form model, application and machine |
1205 |
|
:param model_name: |
1206 |
|
:param application_name: |
1207 |
|
:param machine_id: |
1208 |
|
:return: |
1209 |
|
""" |
1210 |
|
# id for the execution environment |
1211 |
1 |
return "{}.{}.{}".format(model_name, application_name, machine_id) |
1212 |
|
|
1213 |
1 |
@staticmethod |
1214 |
1 |
def _get_ee_id_components(ee_id: str) -> (str, str, str): |
1215 |
|
""" |
1216 |
|
Get model, application and machine components from an execution environment id |
1217 |
|
:param ee_id: |
1218 |
|
:return: model_name, application_name, machine_id |
1219 |
|
""" |
1220 |
|
|
1221 |
0 |
return get_ee_id_components(ee_id) |
1222 |
|
|
1223 |
1 |
@staticmethod |
1224 |
1 |
def _find_charm_level(vnf_id: str, vdu_id: str) -> str: |
1225 |
|
"""Decides the charm level. |
1226 |
|
Args: |
1227 |
|
vnf_id (str): VNF id |
1228 |
|
vdu_id (str): VDU id |
1229 |
|
|
1230 |
|
Returns: |
1231 |
|
charm_level (str): ns-level or vnf-level or vdu-level |
1232 |
|
""" |
1233 |
1 |
if vdu_id and not vnf_id: |
1234 |
1 |
raise N2VCException(message="If vdu-id exists, vnf-id should be provided.") |
1235 |
1 |
if vnf_id and vdu_id: |
1236 |
1 |
return "vdu-level" |
1237 |
1 |
if vnf_id and not vdu_id: |
1238 |
1 |
return "vnf-level" |
1239 |
1 |
if not vnf_id and not vdu_id: |
1240 |
1 |
return "ns-level" |
1241 |
|
|
1242 |
1 |
@staticmethod |
1243 |
1 |
def _generate_backward_compatible_application_name( |
1244 |
|
vnf_id: str, vdu_id: str, vdu_count: str |
1245 |
|
) -> str: |
1246 |
|
"""Generate backward compatible application name |
1247 |
|
by limiting the app name to 50 characters. |
1248 |
|
|
1249 |
|
Args: |
1250 |
|
vnf_id (str): VNF ID |
1251 |
|
vdu_id (str): VDU ID |
1252 |
|
vdu_count (str): vdu-count-index |
1253 |
|
|
1254 |
|
Returns: |
1255 |
|
application_name (str): generated application name |
1256 |
|
|
1257 |
|
""" |
1258 |
1 |
if vnf_id is None or len(vnf_id) == 0: |
1259 |
1 |
vnf_id = "" |
1260 |
|
else: |
1261 |
|
# Shorten the vnf_id to its last twelve characters |
1262 |
1 |
vnf_id = "vnf-" + vnf_id[-12:] |
1263 |
|
|
1264 |
1 |
if vdu_id is None or len(vdu_id) == 0: |
1265 |
1 |
vdu_id = "" |
1266 |
|
else: |
1267 |
|
# Shorten the vdu_id to its last twelve characters |
1268 |
1 |
vdu_id = "-vdu-" + vdu_id[-12:] |
1269 |
|
|
1270 |
1 |
if vdu_count is None or len(vdu_count) == 0: |
1271 |
1 |
vdu_count = "" |
1272 |
|
else: |
1273 |
1 |
vdu_count = "-cnt-" + vdu_count |
1274 |
|
|
1275 |
|
# Generate a random suffix with 5 characters (the default size used by K8s) |
1276 |
1 |
random_suffix = generate_random_alfanum_string(size=5) |
1277 |
|
|
1278 |
1 |
application_name = "app-{}{}{}-{}".format( |
1279 |
|
vnf_id, vdu_id, vdu_count, random_suffix |
1280 |
|
) |
1281 |
1 |
return application_name |
1282 |
|
|
1283 |
1 |
@staticmethod |
1284 |
1 |
def _get_vca_record(search_key: str, vca_records: list, vdu_id: str) -> dict: |
1285 |
|
"""Get the correct VCA record dict depending on the search key |
1286 |
|
|
1287 |
|
Args: |
1288 |
|
search_key (str): keyword to find the correct VCA record |
1289 |
|
vca_records (list): All VCA records as list |
1290 |
|
vdu_id (str): VDU ID |
1291 |
|
|
1292 |
|
Returns: |
1293 |
|
vca_record (dict): Dictionary which includes the correct VCA record |
1294 |
|
|
1295 |
|
""" |
1296 |
1 |
return next( |
1297 |
|
filter(lambda record: record[search_key] == vdu_id, vca_records), {} |
1298 |
|
) |
1299 |
|
|
1300 |
1 |
@staticmethod |
1301 |
1 |
def _generate_application_name( |
1302 |
|
charm_level: str, |
1303 |
|
vnfrs: dict, |
1304 |
|
vca_records: list, |
1305 |
|
vnf_count: str = None, |
1306 |
|
vdu_id: str = None, |
1307 |
|
vdu_count: str = None, |
1308 |
|
) -> str: |
1309 |
|
"""Generate application name to make the relevant charm of VDU/KDU |
1310 |
|
in the VNFD descriptor become clearly visible. |
1311 |
|
Limiting the app name to 50 characters. |
1312 |
|
|
1313 |
|
Args: |
1314 |
|
charm_level (str): level of charm |
1315 |
|
vnfrs (dict): vnf record dict |
1316 |
|
vca_records (list): db_nsr["_admin"]["deployed"]["VCA"] as list |
1317 |
|
vnf_count (str): vnf count index |
1318 |
|
vdu_id (str): VDU ID |
1319 |
|
vdu_count (str): vdu count index |
1320 |
|
|
1321 |
|
Returns: |
1322 |
|
application_name (str): generated application name |
1323 |
|
|
1324 |
|
""" |
1325 |
1 |
application_name = "" |
1326 |
1 |
if charm_level == "ns-level": |
1327 |
1 |
if len(vca_records) != 1: |
1328 |
1 |
raise N2VCException(message="One VCA record is expected.") |
1329 |
|
# Only one VCA record is expected if it's ns-level charm. |
1330 |
|
# Shorten the charm name to its first 40 characters. |
1331 |
1 |
charm_name = vca_records[0]["charm_name"][:40] |
1332 |
1 |
if not charm_name: |
1333 |
1 |
raise N2VCException(message="Charm name should be provided.") |
1334 |
1 |
application_name = charm_name + "-ns" |
1335 |
|
|
1336 |
1 |
elif charm_level == "vnf-level": |
1337 |
1 |
if len(vca_records) < 1: |
1338 |
1 |
raise N2VCException(message="One or more VCA record is expected.") |
1339 |
|
# If VNF is scaled, more than one VCA record may be included in vca_records |
1340 |
|
# but ee_descriptor_id is same. |
1341 |
|
# Shorten the ee_descriptor_id and member-vnf-index-ref |
1342 |
|
# to first 12 characters. |
1343 |
1 |
application_name = ( |
1344 |
|
vca_records[0]["ee_descriptor_id"][:12] |
1345 |
|
+ "-" |
1346 |
|
+ vnf_count |
1347 |
|
+ "-" |
1348 |
|
+ vnfrs["member-vnf-index-ref"][:12] |
1349 |
|
+ "-vnf" |
1350 |
|
) |
1351 |
1 |
elif charm_level == "vdu-level": |
1352 |
1 |
if len(vca_records) < 1: |
1353 |
0 |
raise N2VCException(message="One or more VCA record is expected.") |
1354 |
|
|
1355 |
|
# Charms are also used for deployments with Helm charts. |
1356 |
|
# If deployment unit is a Helm chart/KDU, |
1357 |
|
# vdu_profile_id and vdu_count will be empty string. |
1358 |
1 |
if vdu_count is None: |
1359 |
1 |
vdu_count = "" |
1360 |
|
|
1361 |
|
# If vnf/vdu is scaled, more than one VCA record may be included in vca_records |
1362 |
|
# but ee_descriptor_id is same. |
1363 |
|
# Shorten the ee_descriptor_id, member-vnf-index-ref and vdu_profile_id |
1364 |
|
# to first 12 characters. |
1365 |
1 |
if not vdu_id: |
1366 |
1 |
raise N2VCException(message="vdu-id should be provided.") |
1367 |
|
|
1368 |
1 |
vca_record = N2VCJujuConnector._get_vca_record( |
1369 |
|
"vdu_id", vca_records, vdu_id |
1370 |
|
) |
1371 |
|
|
1372 |
1 |
if not vca_record: |
1373 |
1 |
vca_record = N2VCJujuConnector._get_vca_record( |
1374 |
|
"kdu_name", vca_records, vdu_id |
1375 |
|
) |
1376 |
|
|
1377 |
1 |
application_name = ( |
1378 |
|
vca_record["ee_descriptor_id"][:12] |
1379 |
|
+ "-" |
1380 |
|
+ vnf_count |
1381 |
|
+ "-" |
1382 |
|
+ vnfrs["member-vnf-index-ref"][:12] |
1383 |
|
+ "-" |
1384 |
|
+ vdu_id[:12] |
1385 |
|
+ "-" |
1386 |
|
+ vdu_count |
1387 |
|
+ "-vdu" |
1388 |
|
) |
1389 |
|
|
1390 |
1 |
return application_name |
1391 |
|
|
1392 |
1 |
def _get_vnf_count_and_record( |
1393 |
|
self, charm_level: str, vnf_id_and_count: str |
1394 |
|
) -> Tuple[str, dict]: |
1395 |
|
"""Get the vnf count and VNF record depend on charm level |
1396 |
|
|
1397 |
|
Args: |
1398 |
|
charm_level (str) |
1399 |
|
vnf_id_and_count (str) |
1400 |
|
|
1401 |
|
Returns: |
1402 |
|
(vnf_count (str), db_vnfr(dict)) as Tuple |
1403 |
|
|
1404 |
|
""" |
1405 |
1 |
vnf_count = "" |
1406 |
1 |
db_vnfr = {} |
1407 |
|
|
1408 |
1 |
if charm_level in ("vnf-level", "vdu-level"): |
1409 |
1 |
vnf_id = "-".join(vnf_id_and_count.split("-")[:-1]) |
1410 |
1 |
vnf_count = vnf_id_and_count.split("-")[-1] |
1411 |
1 |
db_vnfr = self.db.get_one("vnfrs", {"_id": vnf_id}) |
1412 |
|
|
1413 |
|
# If the charm is ns level, it returns empty vnf_count and db_vnfr |
1414 |
1 |
return vnf_count, db_vnfr |
1415 |
|
|
1416 |
1 |
@staticmethod |
1417 |
1 |
def _get_vca_records(charm_level: str, db_nsr: dict, db_vnfr: dict) -> list: |
1418 |
|
"""Get the VCA records from db_nsr dict |
1419 |
|
|
1420 |
|
Args: |
1421 |
|
charm_level (str): level of charm |
1422 |
|
db_nsr (dict): NS record from database |
1423 |
|
db_vnfr (dict): VNF record from database |
1424 |
|
|
1425 |
|
Returns: |
1426 |
|
vca_records (list): List of VCA record dictionaries |
1427 |
|
|
1428 |
|
""" |
1429 |
1 |
vca_records = {} |
1430 |
1 |
if charm_level == "ns-level": |
1431 |
1 |
vca_records = list( |
1432 |
|
filter( |
1433 |
|
lambda vca_record: vca_record["target_element"] == "ns", |
1434 |
|
db_nsr["_admin"]["deployed"]["VCA"], |
1435 |
|
) |
1436 |
|
) |
1437 |
1 |
elif charm_level in ["vnf-level", "vdu-level"]: |
1438 |
1 |
vca_records = list( |
1439 |
|
filter( |
1440 |
|
lambda vca_record: vca_record["member-vnf-index"] |
1441 |
|
== db_vnfr["member-vnf-index-ref"], |
1442 |
|
db_nsr["_admin"]["deployed"]["VCA"], |
1443 |
|
) |
1444 |
|
) |
1445 |
|
|
1446 |
1 |
return vca_records |
1447 |
|
|
1448 |
1 |
def _get_application_name(self, namespace: str) -> str: |
1449 |
|
"""Build application name from namespace |
1450 |
|
|
1451 |
|
Application name structure: |
1452 |
|
NS level: <charm-name>-ns |
1453 |
|
VNF level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-vnf |
1454 |
|
VDU level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>- |
1455 |
|
<vdu-profile-id>-z<vdu-ordinal-scale-number>-vdu |
1456 |
|
|
1457 |
|
Application naming for backward compatibility (old structure): |
1458 |
|
NS level: app-<random_value> |
1459 |
|
VNF level: app-vnf-<vnf-id>-z<ordinal-scale-number>-<random_value> |
1460 |
|
VDU level: app-vnf-<vnf-id>-z<vnf-ordinal-scale-number>-vdu- |
1461 |
|
<vdu-id>-cnt-<vdu-count>-z<vdu-ordinal-scale-number>-<random_value> |
1462 |
|
|
1463 |
|
Args: |
1464 |
|
namespace (str) |
1465 |
|
|
1466 |
|
Returns: |
1467 |
|
application_name (str) |
1468 |
|
|
1469 |
|
""" |
1470 |
|
# split namespace components |
1471 |
1 |
( |
1472 |
|
nsi_id, |
1473 |
|
ns_id, |
1474 |
|
vnf_id_and_count, |
1475 |
|
vdu_id, |
1476 |
|
vdu_count, |
1477 |
|
) = self._get_namespace_components(namespace=namespace) |
1478 |
|
|
1479 |
1 |
if not ns_id: |
1480 |
0 |
raise N2VCException(message="ns-id should be provided.") |
1481 |
|
|
1482 |
1 |
charm_level = self._find_charm_level(vnf_id_and_count, vdu_id) |
1483 |
1 |
db_nsr = self.db.get_one("nsrs", {"_id": ns_id}) |
1484 |
1 |
vnf_count, db_vnfr = self._get_vnf_count_and_record( |
1485 |
|
charm_level, vnf_id_and_count |
1486 |
|
) |
1487 |
1 |
vca_records = self._get_vca_records(charm_level, db_nsr, db_vnfr) |
1488 |
|
|
1489 |
1 |
if all("charm_name" in vca_record.keys() for vca_record in vca_records): |
1490 |
1 |
application_name = self._generate_application_name( |
1491 |
|
charm_level, |
1492 |
|
db_vnfr, |
1493 |
|
vca_records, |
1494 |
|
vnf_count=vnf_count, |
1495 |
|
vdu_id=vdu_id, |
1496 |
|
vdu_count=vdu_count, |
1497 |
|
) |
1498 |
|
else: |
1499 |
1 |
application_name = self._generate_backward_compatible_application_name( |
1500 |
|
vnf_id_and_count, vdu_id, vdu_count |
1501 |
|
) |
1502 |
|
|
1503 |
1 |
return N2VCJujuConnector._format_app_name(application_name) |
1504 |
|
|
1505 |
1 |
@staticmethod |
1506 |
1 |
def _format_model_name(name: str) -> str: |
1507 |
|
"""Format the name of the model. |
1508 |
|
|
1509 |
|
Model names may only contain lowercase letters, digits and hyphens |
1510 |
|
""" |
1511 |
|
|
1512 |
0 |
return name.replace("_", "-").replace(" ", "-").lower() |
1513 |
|
|
1514 |
1 |
@staticmethod |
1515 |
1 |
def _format_app_name(name: str) -> str: |
1516 |
|
"""Format the name of the application (in order to assure valid application name). |
1517 |
|
|
1518 |
|
Application names have restrictions (run juju deploy --help): |
1519 |
|
- contains lowercase letters 'a'-'z' |
1520 |
|
- contains numbers '0'-'9' |
1521 |
|
- contains hyphens '-' |
1522 |
|
- starts with a lowercase letter |
1523 |
|
- not two or more consecutive hyphens |
1524 |
|
- after a hyphen, not a group with all numbers |
1525 |
|
""" |
1526 |
|
|
1527 |
1 |
def all_numbers(s: str) -> bool: |
1528 |
1 |
for c in s: |
1529 |
1 |
if not c.isdigit(): |
1530 |
1 |
return False |
1531 |
1 |
return True |
1532 |
|
|
1533 |
1 |
new_name = name.replace("_", "-") |
1534 |
1 |
new_name = new_name.replace(" ", "-") |
1535 |
1 |
new_name = new_name.lower() |
1536 |
1 |
while new_name.find("--") >= 0: |
1537 |
1 |
new_name = new_name.replace("--", "-") |
1538 |
1 |
groups = new_name.split("-") |
1539 |
|
|
1540 |
|
# find 'all numbers' groups and prefix them with a letter |
1541 |
1 |
app_name = "" |
1542 |
1 |
for i in range(len(groups)): |
1543 |
1 |
group = groups[i] |
1544 |
1 |
if all_numbers(group): |
1545 |
1 |
group = "z" + group |
1546 |
1 |
if i > 0: |
1547 |
1 |
app_name += "-" |
1548 |
1 |
app_name += group |
1549 |
|
|
1550 |
1 |
if app_name[0].isdigit(): |
1551 |
0 |
app_name = "z" + app_name |
1552 |
|
|
1553 |
1 |
return app_name |
1554 |
|
|
1555 |
1 |
async def validate_vca(self, vca_id: str): |
1556 |
|
""" |
1557 |
|
Validate a VCA by connecting/disconnecting to/from it |
1558 |
|
|
1559 |
|
:param: vca_id: VCA ID |
1560 |
|
""" |
1561 |
0 |
vca_connection = await get_connection(self._store, vca_id=vca_id) |
1562 |
0 |
libjuju = Libjuju(vca_connection, log=self.log, n2vc=self) |
1563 |
0 |
controller = await libjuju.get_controller() |
1564 |
0 |
await libjuju.disconnect_controller(controller) |