Pin black version in tox.ini to 23.12.1
[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, retry_callback
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 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 N2VCConnector.__init__(self, db=db, fs=fs, log=log, on_update_db=on_update_db)
77
78 # silence websocket traffic log
79 logging.getLogger("websockets.protocol").setLevel(logging.INFO)
80 logging.getLogger("juju.client.connection").setLevel(logging.WARN)
81 logging.getLogger("model").setLevel(logging.WARN)
82
83 self.log.info("Initializing N2VC juju connector...")
84
85 db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
86 self._store = MotorStore(db_uri)
87 self.loading_libjuju = asyncio.Lock()
88 self.delete_namespace_locks = {}
89 self.log.info("N2VC juju connector initialized")
90
91 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 libjuju = await self._get_libjuju(vca_id)
108
109 _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 model_name = ns_id
114 if model_name is None:
115 msg = "Namespace {} not valid".format(namespace)
116 self.log.error(msg)
117 raise N2VCBadArgumentsException(msg, ["namespace"])
118
119 status = {}
120 models = await libjuju.list_models(contains=ns_id)
121
122 for m in models:
123 status[m] = await libjuju.get_model_status(m)
124
125 if yaml_format:
126 return obj_to_yaml(status)
127 else:
128 return obj_to_dict(status)
129
130 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 try:
140 libjuju = await self._get_libjuju(vca_id)
141 for model_name in vcastatus:
142 # Adding executed actions
143 vcastatus[model_name][
144 "executedActions"
145 ] = await libjuju.get_executed_actions(model_name)
146 for application in vcastatus[model_name]["applications"]:
147 # Adding application actions
148 vcastatus[model_name]["applications"][application][
149 "actions"
150 ] = await libjuju.get_actions(application, model_name)
151 # Adding application configs
152 vcastatus[model_name]["applications"][application][
153 "configs"
154 ] = await libjuju.get_application_configs(model_name, application)
155 except Exception as e:
156 self.log.debug("Error in updating vca status: {}".format(str(e)))
157
158 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 self.log.info(
188 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
189 namespace, reuse_ee_id
190 )
191 )
192 libjuju = await self._get_libjuju(vca_id)
193
194 machine_id = None
195 if reuse_ee_id:
196 model_name, application_name, machine_id = self._get_ee_id_components(
197 ee_id=reuse_ee_id
198 )
199 else:
200 (
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 model_name = ns_id
209 # application name
210 application_name = self._get_application_name(namespace=namespace)
211
212 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 try:
220 if not await libjuju.model_exists(model_name):
221 await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud)
222 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 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 self.log.debug("ee_id: {}".format(ee_id))
236
237 if new:
238 # write ee_id in database
239 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
240
241 except Exception as e:
242 message = "Error creating machine on juju: {}".format(e)
243 self.log.error(message)
244 raise N2VCException(message=message)
245
246 # new machine credentials
247 credentials = {"hostname": machine.dns_name}
248
249 self.log.info(
250 "Execution environment created. ee_id: {}, credentials: {}".format(
251 ee_id, credentials
252 )
253 )
254
255 return ee_id, credentials
256
257 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 self.log.info(
287 "Registering execution environment. namespace={}, credentials={}".format(
288 namespace, credentials
289 )
290 )
291 libjuju = await self._get_libjuju(vca_id)
292
293 if credentials is None:
294 raise N2VCBadArgumentsException(
295 message="credentials are mandatory", bad_args=["credentials"]
296 )
297 if credentials.get("hostname"):
298 hostname = credentials["hostname"]
299 else:
300 raise N2VCBadArgumentsException(
301 message="hostname is mandatory", bad_args=["credentials.hostname"]
302 )
303 if credentials.get("username"):
304 username = credentials["username"]
305 else:
306 raise N2VCBadArgumentsException(
307 message="username is mandatory", bad_args=["credentials.username"]
308 )
309 if "private_key_path" in credentials:
310 private_key_path = credentials["private_key_path"]
311 else:
312 # if not passed as argument, use generated private key path
313 private_key_path = self.private_key_path
314
315 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
316 namespace=namespace
317 )
318
319 # model name
320 model_name = ns_id
321 # application name
322 application_name = self._get_application_name(namespace=namespace)
323
324 # register machine on juju
325 try:
326 if not await libjuju.model_exists(model_name):
327 await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud)
328 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 except Exception as e:
338 self.log.error("Error registering machine: {}".format(e))
339 raise N2VCException(
340 message="Error registering machine on juju: {}".format(e)
341 )
342
343 self.log.info("Machine registered: {}".format(machine_id))
344
345 # id for the execution environment
346 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 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
353
354 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 @retry(
359 attempts=3,
360 delay=5,
361 retry_exceptions=(N2VCApplicationExists,),
362 timeout=None,
363 callback=retry_callback,
364 )
365 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 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 libjuju = await self._get_libjuju(vca_id)
409
410 # check arguments
411 if ee_id is None or len(ee_id) == 0:
412 raise N2VCBadArgumentsException(
413 message="ee_id is mandatory", bad_args=["ee_id"]
414 )
415 if artifact_path is None or len(artifact_path) == 0:
416 raise N2VCBadArgumentsException(
417 message="artifact_path is mandatory", bad_args=["artifact_path"]
418 )
419 if db_dict is None:
420 raise N2VCBadArgumentsException(
421 message="db_dict is mandatory", bad_args=["db_dict"]
422 )
423
424 try:
425 (
426 model_name,
427 application_name,
428 machine_id,
429 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
430 self.log.debug(
431 "model: {}, application: {}, machine: {}".format(
432 model_name, application_name, machine_id
433 )
434 )
435 except Exception:
436 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 while artifact_path.find("//") >= 0:
445 artifact_path = artifact_path.replace("//", "/")
446
447 # check charm path
448 if not self.fs.file_exists(artifact_path):
449 msg = "artifact path does not exist: {}".format(artifact_path)
450 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
451
452 if artifact_path.startswith("/"):
453 full_path = self.fs.path + artifact_path
454 else:
455 full_path = self.fs.path + "/" + artifact_path
456
457 try:
458 if vca_type == "native_charm" and await libjuju.check_application_exists(
459 model_name, application_name
460 ):
461 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 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 except JujuApplicationExists as e:
482 raise N2VCApplicationExists(
483 message="Error deploying charm into ee={} : {}".format(ee_id, e.message)
484 )
485 except Exception as e:
486 raise N2VCException(
487 message="Error deploying charm into ee={} : {}".format(ee_id, e)
488 )
489
490 self.log.info("Configuration sw installed")
491
492 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 self.log.info(
525 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
526 charm_name, artifact_path, db_dict
527 )
528 )
529 libjuju = await self._get_libjuju(vca_id)
530
531 if artifact_path is None or len(artifact_path) == 0:
532 raise N2VCBadArgumentsException(
533 message="artifact_path is mandatory", bad_args=["artifact_path"]
534 )
535 if db_dict is None:
536 raise N2VCBadArgumentsException(
537 message="db_dict is mandatory", bad_args=["db_dict"]
538 )
539
540 # remove // in charm path
541 while artifact_path.find("//") >= 0:
542 artifact_path = artifact_path.replace("//", "/")
543
544 # check charm path
545 if not self.fs.file_exists(artifact_path):
546 msg = "artifact path does not exist: {}".format(artifact_path)
547 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
548
549 if artifact_path.startswith("/"):
550 full_path = self.fs.path + artifact_path
551 else:
552 full_path = self.fs.path + "/" + artifact_path
553
554 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
555 model_name = "{}-k8s".format(ns_id)
556 if not await libjuju.model_exists(model_name):
557 await libjuju.add_model(model_name, libjuju.vca_connection.k8s_cloud)
558 application_name = self._get_application_name(namespace)
559
560 try:
561 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 except Exception as e:
572 raise N2VCException(message="Error deploying charm: {}".format(e))
573
574 self.log.info("K8s proxy charm installed")
575 ee_id = N2VCJujuConnector._build_ee_id(
576 model_name=model_name, application_name=application_name, machine_id="k8s"
577 )
578
579 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
580
581 return ee_id
582
583 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 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 libjuju = await self._get_libjuju(vca_id)
616
617 # check arguments
618 if ee_id is None or len(ee_id) == 0:
619 raise N2VCBadArgumentsException(
620 message="ee_id is mandatory", bad_args=["ee_id"]
621 )
622 if db_dict is None:
623 raise N2VCBadArgumentsException(
624 message="db_dict is mandatory", bad_args=["db_dict"]
625 )
626
627 try:
628 (
629 model_name,
630 application_name,
631 machine_id,
632 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
633 self.log.debug(
634 "model: {}, application: {}, machine: {}".format(
635 model_name, application_name, machine_id
636 )
637 )
638 except Exception:
639 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 output = None
651
652 application_name = N2VCJujuConnector._format_app_name(application_name)
653
654 # execute action: generate-ssh-key
655 try:
656 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 except Exception as e:
665 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 try:
673 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 except Exception as e:
682 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
683 self.log.info(msg)
684 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
685
686 # return public key if exists
687 return output["pubkey"] if "pubkey" in output else output
688
689 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 libjuju = await self._get_libjuju(vca_id)
702 return await libjuju.get_metrics(model_name, application_name)
703
704 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 self.log.debug(f"adding new relation between {provider} and {requirer}")
714 cross_model_relation = (
715 provider.model_name != requirer.model_name
716 or provider.vca_id != requirer.vca_id
717 )
718 try:
719 if cross_model_relation:
720 # Cross-model relation
721 provider_libjuju = await self._get_libjuju(provider.vca_id)
722 requirer_libjuju = await self._get_libjuju(requirer.vca_id)
723 offer = await provider_libjuju.offer(provider)
724 if offer:
725 saas_name = await requirer_libjuju.consume(
726 requirer.model_name, offer, provider_libjuju
727 )
728 await requirer_libjuju.add_relation(
729 requirer.model_name, requirer.endpoint, saas_name
730 )
731 else:
732 # Standard relation
733 vca_id = provider.vca_id
734 model = provider.model_name
735 libjuju = await self._get_libjuju(vca_id)
736 # add juju relations between two applications
737 await libjuju.add_relation(
738 model_name=model,
739 endpoint_1=provider.endpoint,
740 endpoint_2=requirer.endpoint,
741 )
742 except Exception as e:
743 message = f"Error adding relation between {provider} and {requirer}: {e}"
744 self.log.error(message)
745 raise N2VCException(message=message)
746
747 async def remove_relation(self):
748 # TODO
749 self.log.info("Method not implemented yet")
750 raise MethodNotImplemented()
751
752 async def deregister_execution_environments(self):
753 self.log.info("Method not implemented yet")
754 raise MethodNotImplemented()
755
756 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 self.log.info("Deleting namespace={}".format(namespace))
775 will_not_delete = False
776 if namespace not in self.delete_namespace_locks:
777 self.delete_namespace_locks[namespace] = asyncio.Lock()
778 delete_lock = self.delete_namespace_locks[namespace]
779
780 while delete_lock.locked():
781 will_not_delete = True
782 await asyncio.sleep(0.1)
783
784 if will_not_delete:
785 self.log.info("Namespace {} deleted by another worker.".format(namespace))
786 return
787
788 try:
789 async with delete_lock:
790 libjuju = await self._get_libjuju(vca_id)
791
792 # check arguments
793 if namespace is None:
794 raise N2VCBadArgumentsException(
795 message="namespace is mandatory", bad_args=["namespace"]
796 )
797
798 (
799 _nsi_id,
800 ns_id,
801 _vnf_id,
802 _vdu_id,
803 _vdu_count,
804 ) = self._get_namespace_components(namespace=namespace)
805 if ns_id is not None:
806 try:
807 models = await libjuju.list_models(contains=ns_id)
808 for model in models:
809 await libjuju.destroy_model(
810 model_name=model, total_timeout=total_timeout
811 )
812 except Exception as e:
813 self.log.error(f"Error deleting namespace {namespace} : {e}")
814 raise N2VCException(
815 message="Error deleting namespace {} : {}".format(
816 namespace, e
817 )
818 )
819 else:
820 raise N2VCBadArgumentsException(
821 message="only ns_id is permitted to delete yet",
822 bad_args=["namespace"],
823 )
824 except Exception as e:
825 self.log.error(f"Error deleting namespace {namespace} : {e}")
826 raise e
827 finally:
828 self.delete_namespace_locks.pop(namespace)
829 self.log.info("Namespace {} deleted".format(namespace))
830
831 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 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
856 libjuju = await self._get_libjuju(vca_id)
857
858 # check arguments
859 if ee_id is None:
860 raise N2VCBadArgumentsException(
861 message="ee_id is mandatory", bad_args=["ee_id"]
862 )
863
864 model_name, application_name, machine_id = self._get_ee_id_components(
865 ee_id=ee_id
866 )
867 try:
868 if application_to_delete == application_name:
869 # destroy the application
870 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 controller = await libjuju.get_controller()
877 model = await libjuju.get_model(
878 controller=controller,
879 model_name=model_name,
880 )
881 if not model.applications:
882 self.log.info("Model {} is empty, deleting it".format(model_name))
883 await libjuju.destroy_model(
884 model_name=model_name,
885 total_timeout=total_timeout,
886 )
887 elif not scaling_in:
888 # destroy the model
889 await libjuju.destroy_model(
890 model_name=model_name, 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, model_name=model_name
995 )
996 self.log.debug(
997 "Application {} has these actions: {}".format(
998 application_name, actions
999 )
1000 )
1001 if "verify-ssh-credentials" in actions:
1002 # execute verify-credentials
1003 num_retries = 20
1004 retry_timeout = 15.0
1005 for _ in range(num_retries):
1006 try:
1007 self.log.debug("Executing action verify-ssh-credentials...")
1008 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 if ok == "failed":
1018 self.log.debug(
1019 "Error executing verify-ssh-credentials: {}. Retrying..."
1020 )
1021 await asyncio.sleep(retry_timeout)
1022
1023 continue
1024 self.log.debug("Result: {}, output: {}".format(ok, output))
1025 break
1026 except asyncio.CancelledError:
1027 raise
1028 else:
1029 self.log.error(
1030 "Error executing verify-ssh-credentials after {} retries. ".format(
1031 num_retries
1032 )
1033 )
1034 else:
1035 msg = "Action verify-ssh-credentials does not exist in application {}".format(
1036 application_name
1037 )
1038 self.log.debug(msg=msg)
1039 except Exception as e:
1040 self.log.error("Error configuring juju application: {}".format(e))
1041 raise N2VCExecutionException(
1042 message="Error configuring application into ee={} : {}".format(
1043 ee_id, e
1044 ),
1045 primitive_name=primitive_name,
1046 )
1047 return "CONFIG OK"
1048 else:
1049 try:
1050 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 if status == "completed":
1061 return output
1062 else:
1063 if "output" in output:
1064 raise Exception(f'{status}: {output["output"]}')
1065 else:
1066 raise Exception(
1067 f"{status}: No further information received from action"
1068 )
1069
1070 except Exception as e:
1071 self.log.error(f"Error executing primitive {primitive_name}: {e}")
1072 raise N2VCExecutionException(
1073 message=f"Error executing primitive {primitive_name} in ee={ee_id}: {e}",
1074 primitive_name=primitive_name,
1075 )
1076
1077 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 self.log.info("Upgrading charm: {} on ee: {}".format(path, ee_id))
1099 libjuju = await self._get_libjuju(charm_id)
1100
1101 # check arguments
1102 if ee_id is None or len(ee_id) == 0:
1103 raise N2VCBadArgumentsException(
1104 message="ee_id is mandatory", bad_args=["ee_id"]
1105 )
1106 try:
1107 (
1108 model_name,
1109 application_name,
1110 machine_id,
1111 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
1112
1113 except Exception:
1114 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 try:
1122 await libjuju.upgrade_charm(
1123 application_name=application_name,
1124 path=path,
1125 model_name=model_name,
1126 total_timeout=timeout,
1127 )
1128
1129 return f"Charm upgraded with application name {application_name}"
1130
1131 except Exception as e:
1132 self.log.error("Error upgrading charm {}: {}".format(path, e))
1133
1134 raise N2VCException(
1135 message="Error upgrading charm {} in ee={} : {}".format(path, ee_id, e)
1136 )
1137
1138 async def disconnect(self, vca_id: str = None):
1139 """
1140 Disconnect from VCA
1141
1142 :param: vca_id: VCA ID
1143 """
1144 self.log.info("closing juju N2VC...")
1145 libjuju = await self._get_libjuju(vca_id)
1146 try:
1147 await libjuju.disconnect()
1148 except Exception as e:
1149 raise N2VCConnectionException(
1150 message="Error disconnecting controller: {}".format(e),
1151 url=libjuju.vca_connection.data.endpoints,
1152 )
1153
1154 """
1155 ####################################################################################
1156 ################################### P R I V A T E ##################################
1157 ####################################################################################
1158 """
1159
1160 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 if not vca_id:
1169 while self.loading_libjuju.locked():
1170 await asyncio.sleep(0.1)
1171 if not self.libjuju:
1172 async with self.loading_libjuju:
1173 vca_connection = await get_connection(self._store)
1174 self.libjuju = Libjuju(vca_connection, log=self.log)
1175 return self.libjuju
1176 else:
1177 vca_connection = await get_connection(self._store, vca_id)
1178 return Libjuju(vca_connection, log=self.log, n2vc=self)
1179
1180 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
1181 # write ee_id to database: _admin.deployed.VCA.x
1182 try:
1183 the_table = db_dict["collection"]
1184 the_filter = db_dict["filter"]
1185 the_path = db_dict["path"]
1186 if not the_path[-1] == ".":
1187 the_path = the_path + "."
1188 update_dict = {the_path + "ee_id": ee_id}
1189 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1190 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 except asyncio.CancelledError:
1197 raise
1198 except Exception as e:
1199 self.log.error("Error writing ee_id to database: {}".format(e))
1200
1201 @staticmethod
1202 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 return "{}.{}.{}".format(model_name, application_name, machine_id)
1212
1213 @staticmethod
1214 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 return get_ee_id_components(ee_id)
1222
1223 @staticmethod
1224 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 if vdu_id and not vnf_id:
1234 raise N2VCException(message="If vdu-id exists, vnf-id should be provided.")
1235 if vnf_id and vdu_id:
1236 return "vdu-level"
1237 if vnf_id and not vdu_id:
1238 return "vnf-level"
1239 if not vnf_id and not vdu_id:
1240 return "ns-level"
1241
1242 @staticmethod
1243 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 if vnf_id is None or len(vnf_id) == 0:
1259 vnf_id = ""
1260 else:
1261 # Shorten the vnf_id to its last twelve characters
1262 vnf_id = "vnf-" + vnf_id[-12:]
1263
1264 if vdu_id is None or len(vdu_id) == 0:
1265 vdu_id = ""
1266 else:
1267 # Shorten the vdu_id to its last twelve characters
1268 vdu_id = "-vdu-" + vdu_id[-12:]
1269
1270 if vdu_count is None or len(vdu_count) == 0:
1271 vdu_count = ""
1272 else:
1273 vdu_count = "-cnt-" + vdu_count
1274
1275 # Generate a random suffix with 5 characters (the default size used by K8s)
1276 random_suffix = generate_random_alfanum_string(size=5)
1277
1278 application_name = "app-{}{}{}-{}".format(
1279 vnf_id, vdu_id, vdu_count, random_suffix
1280 )
1281 return application_name
1282
1283 @staticmethod
1284 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 return next(
1297 filter(lambda record: record[search_key] == vdu_id, vca_records), {}
1298 )
1299
1300 @staticmethod
1301 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 application_name = ""
1326 if charm_level == "ns-level":
1327 if len(vca_records) != 1:
1328 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 charm_name = vca_records[0]["charm_name"][:40]
1332 if not charm_name:
1333 raise N2VCException(message="Charm name should be provided.")
1334 application_name = charm_name + "-ns"
1335
1336 elif charm_level == "vnf-level":
1337 if len(vca_records) < 1:
1338 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 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 elif charm_level == "vdu-level":
1352 if len(vca_records) < 1:
1353 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 if vdu_count is None:
1359 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 if not vdu_id:
1366 raise N2VCException(message="vdu-id should be provided.")
1367
1368 vca_record = N2VCJujuConnector._get_vca_record(
1369 "vdu_id", vca_records, vdu_id
1370 )
1371
1372 if not vca_record:
1373 vca_record = N2VCJujuConnector._get_vca_record(
1374 "kdu_name", vca_records, vdu_id
1375 )
1376
1377 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 return application_name
1391
1392 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 vnf_count = ""
1406 db_vnfr = {}
1407
1408 if charm_level in ("vnf-level", "vdu-level"):
1409 vnf_id = "-".join(vnf_id_and_count.split("-")[:-1])
1410 vnf_count = vnf_id_and_count.split("-")[-1]
1411 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 return vnf_count, db_vnfr
1415
1416 @staticmethod
1417 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 vca_records = {}
1430 if charm_level == "ns-level":
1431 vca_records = list(
1432 filter(
1433 lambda vca_record: vca_record["target_element"] == "ns",
1434 db_nsr["_admin"]["deployed"]["VCA"],
1435 )
1436 )
1437 elif charm_level in ["vnf-level", "vdu-level"]:
1438 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 return vca_records
1447
1448 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 (
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 if not ns_id:
1480 raise N2VCException(message="ns-id should be provided.")
1481
1482 charm_level = self._find_charm_level(vnf_id_and_count, vdu_id)
1483 db_nsr = self.db.get_one("nsrs", {"_id": ns_id})
1484 vnf_count, db_vnfr = self._get_vnf_count_and_record(
1485 charm_level, vnf_id_and_count
1486 )
1487 vca_records = self._get_vca_records(charm_level, db_nsr, db_vnfr)
1488
1489 if all("charm_name" in vca_record.keys() for vca_record in vca_records):
1490 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 application_name = self._generate_backward_compatible_application_name(
1500 vnf_id_and_count, vdu_id, vdu_count
1501 )
1502
1503 return N2VCJujuConnector._format_app_name(application_name)
1504
1505 @staticmethod
1506 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 return name.replace("_", "-").replace(" ", "-").lower()
1513
1514 @staticmethod
1515 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 def all_numbers(s: str) -> bool:
1528 for c in s:
1529 if not c.isdigit():
1530 return False
1531 return True
1532
1533 new_name = name.replace("_", "-")
1534 new_name = new_name.replace(" ", "-")
1535 new_name = new_name.lower()
1536 while new_name.find("--") >= 0:
1537 new_name = new_name.replace("--", "-")
1538 groups = new_name.split("-")
1539
1540 # find 'all numbers' groups and prefix them with a letter
1541 app_name = ""
1542 for i in range(len(groups)):
1543 group = groups[i]
1544 if all_numbers(group):
1545 group = "z" + group
1546 if i > 0:
1547 app_name += "-"
1548 app_name += group
1549
1550 if app_name[0].isdigit():
1551 app_name = "z" + app_name
1552
1553 return app_name
1554
1555 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 vca_connection = await get_connection(self._store, vca_id=vca_id)
1562 libjuju = Libjuju(vca_connection, log=self.log, n2vc=self)
1563 controller = await libjuju.get_controller()
1564 await libjuju.disconnect_controller(controller)