Code Coverage

Cobertura Coverage Report > n2vc >

n2vc_juju_conn.py

Trend

File Coverage summary

NameClassesLinesConditionals
n2vc_juju_conn.py
100%
1/1
51%
249/490
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
n2vc_juju_conn.py
51%
249/490
N/A

Source

n2vc/n2vc_juju_conn.py
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)