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