2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
26 from n2vc
.config
import EnvironConfig
27 from n2vc
.definitions
import RelationEndpoint
28 from n2vc
.exceptions
import (
29 N2VCBadArgumentsException
,
31 N2VCConnectionException
,
32 N2VCExecutionException
,
33 N2VCApplicationExists
,
34 JujuApplicationExists
,
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
48 class N2VCJujuConnector(N2VCConnector
):
51 ####################################################################################
52 ################################### P U B L I C ####################################
53 ####################################################################################
56 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
70 :param: db: Database object from osm_common
71 :param: fs: Filesystem object from osm_common
73 :param: loop: Asyncio loop
74 :param: on_update_db: Callback function to be called for updating the database.
77 # parent class constructor
78 N2VCConnector
.__init
__(
79 self
, db
=db
, fs
=fs
, log
=log
, loop
=loop
, on_update_db
=on_update_db
82 # silence websocket traffic log
83 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
84 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
85 logging
.getLogger("model").setLevel(logging
.WARN
)
87 self
.log
.info("Initializing N2VC juju connector...")
89 db_uri
= EnvironConfig(prefixes
=["OSMLCM_", "OSMMON_"]).get("database_uri")
90 self
._store
= MotorStore(db_uri
)
91 self
.loading_libjuju
= asyncio
.Lock(loop
=self
.loop
)
92 self
.delete_namespace_locks
= {}
93 self
.log
.info("N2VC juju connector initialized")
96 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
99 Get status from all juju models from a VCA
101 :param namespace: we obtain ns from namespace
102 :param yaml_format: returns a yaml string
103 :param: vca_id: VCA ID from which the status will be retrieved.
105 # TODO: Review where is this function used. It is not optimal at all to get the status
106 # from all the juju models of a particular VCA. Additionally, these models might
107 # not have been deployed by OSM, in that case we are getting information from
108 # deployments outside of OSM's scope.
110 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
111 libjuju
= await self
._get
_libjuju
(vca_id
)
113 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
116 # model name is ns_id
118 if model_name
is None:
119 msg
= "Namespace {} not valid".format(namespace
)
121 raise N2VCBadArgumentsException(msg
, ["namespace"])
124 models
= await libjuju
.list_models(contains
=ns_id
)
127 status
[m
] = await libjuju
.get_model_status(m
)
130 return obj_to_yaml(status
)
132 return obj_to_dict(status
)
134 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
136 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
138 :param vcastatus: dict containing vcaStatus
139 :param: vca_id: VCA ID
144 libjuju
= await self
._get
_libjuju
(vca_id
)
145 for model_name
in vcastatus
:
146 # Adding executed actions
147 vcastatus
[model_name
][
149 ] = await libjuju
.get_executed_actions(model_name
)
150 for application
in vcastatus
[model_name
]["applications"]:
151 # Adding application actions
152 vcastatus
[model_name
]["applications"][application
][
154 ] = await libjuju
.get_actions(application
, model_name
)
155 # Adding application configs
156 vcastatus
[model_name
]["applications"][application
][
158 ] = await libjuju
.get_application_configs(model_name
, application
)
159 except Exception as e
:
160 self
.log
.debug("Error in updating vca status: {}".format(str(e
)))
162 async def create_execution_environment(
166 reuse_ee_id
: str = None,
167 progress_timeout
: float = None,
168 total_timeout
: float = None,
172 Create an Execution Environment. Returns when it is created or raises an
175 :param: namespace: Contains a dot separate string.
176 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
177 :param: db_dict: where to write to database when the status changes.
178 It contains a dictionary with {collection: str, filter: {}, path: str},
179 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
180 "_admin.deployed.VCA.3"}
181 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
183 :param: progress_timeout: Progress timeout
184 :param: total_timeout: Total timeout
185 :param: vca_id: VCA ID
187 :returns: id of the new execution environment and credentials for it
188 (credentials can contains hostname, username, etc depending on underlying cloud)
192 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
193 namespace
, reuse_ee_id
196 libjuju
= await self
._get
_libjuju
(vca_id
)
200 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
210 ) = self
._get
_namespace
_components
(namespace
=namespace
)
211 # model name is ns_id
214 application_name
= self
._get
_application
_name
(namespace
=namespace
)
217 "model name: {}, application name: {}, machine_id: {}".format(
218 model_name
, application_name
, machine_id
222 # create or reuse a new juju machine
224 if not await libjuju
.model_exists(model_name
):
225 await libjuju
.add_model(model_name
, libjuju
.vca_connection
.lxd_cloud
)
226 machine
, new
= await libjuju
.create_machine(
227 model_name
=model_name
,
228 machine_id
=machine_id
,
230 progress_timeout
=progress_timeout
,
231 total_timeout
=total_timeout
,
233 # id for the execution environment
234 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
235 model_name
=model_name
,
236 application_name
=application_name
,
237 machine_id
=str(machine
.entity_id
),
239 self
.log
.debug("ee_id: {}".format(ee_id
))
242 # write ee_id in database
243 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
245 except Exception as e
:
246 message
= "Error creating machine on juju: {}".format(e
)
247 self
.log
.error(message
)
248 raise N2VCException(message
=message
)
250 # new machine credentials
251 credentials
= {"hostname": machine
.dns_name
}
254 "Execution environment created. ee_id: {}, credentials: {}".format(
259 return ee_id
, credentials
261 async def register_execution_environment(
266 progress_timeout
: float = None,
267 total_timeout
: float = None,
271 Register an existing execution environment at the VCA
273 :param: namespace: Contains a dot separate string.
274 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
275 :param: credentials: credentials to access the existing execution environment
276 (it can contains hostname, username, path to private key,
277 etc depending on underlying cloud)
278 :param: db_dict: where to write to database when the status changes.
279 It contains a dictionary with {collection: str, filter: {}, path: str},
280 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
281 "_admin.deployed.VCA.3"}
282 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
284 :param: progress_timeout: Progress timeout
285 :param: total_timeout: Total timeout
286 :param: vca_id: VCA ID
288 :returns: id of the execution environment
291 "Registering execution environment. namespace={}, credentials={}".format(
292 namespace
, credentials
295 libjuju
= await self
._get
_libjuju
(vca_id
)
297 if credentials
is None:
298 raise N2VCBadArgumentsException(
299 message
="credentials are mandatory", bad_args
=["credentials"]
301 if credentials
.get("hostname"):
302 hostname
= credentials
["hostname"]
304 raise N2VCBadArgumentsException(
305 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
307 if credentials
.get("username"):
308 username
= credentials
["username"]
310 raise N2VCBadArgumentsException(
311 message
="username is mandatory", bad_args
=["credentials.username"]
313 if "private_key_path" in credentials
:
314 private_key_path
= credentials
["private_key_path"]
316 # if not passed as argument, use generated private key path
317 private_key_path
= self
.private_key_path
319 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
326 application_name
= self
._get
_application
_name
(namespace
=namespace
)
328 # register machine on juju
330 if not await libjuju
.model_exists(model_name
):
331 await libjuju
.add_model(model_name
, libjuju
.vca_connection
.lxd_cloud
)
332 machine_id
= await libjuju
.provision_machine(
333 model_name
=model_name
,
336 private_key_path
=private_key_path
,
338 progress_timeout
=progress_timeout
,
339 total_timeout
=total_timeout
,
341 except Exception as e
:
342 self
.log
.error("Error registering machine: {}".format(e
))
344 message
="Error registering machine on juju: {}".format(e
)
347 self
.log
.info("Machine registered: {}".format(machine_id
))
349 # id for the execution environment
350 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
351 model_name
=model_name
,
352 application_name
=application_name
,
353 machine_id
=str(machine_id
),
356 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
360 # In case of native_charm is being deployed, if JujuApplicationExists error happens
361 # it will try to add_unit
362 @retry(attempts
=3, delay
=5, retry_exceptions
=(N2VCApplicationExists
,), timeout
=None)
363 async def install_configuration_sw(
368 progress_timeout
: float = None,
369 total_timeout
: float = None,
373 scaling_out
: bool = False,
374 vca_type
: str = None,
377 Install the software inside the execution environment identified by ee_id
379 :param: ee_id: the id of the execution environment returned by
380 create_execution_environment or register_execution_environment
381 :param: artifact_path: where to locate the artifacts (parent folder) using
383 the final artifact path will be a combination of this
384 artifact_path and additional string from the config_dict
386 :param: db_dict: where to write into database when the status changes.
387 It contains a dict with
388 {collection: <str>, filter: {}, path: <str>},
389 e.g. {collection: "nsrs", filter:
390 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
391 :param: progress_timeout: Progress timeout
392 :param: total_timeout: Total timeout
393 :param: config: Dictionary with deployment config information.
394 :param: num_units: Number of units to deploy of a particular charm.
395 :param: vca_id: VCA ID
396 :param: scaling_out: Boolean to indicate if it is a scaling out operation
397 :param: vca_type: VCA type
402 "Installing configuration sw on ee_id: {}, "
403 "artifact path: {}, db_dict: {}"
404 ).format(ee_id
, artifact_path
, db_dict
)
406 libjuju
= await self
._get
_libjuju
(vca_id
)
409 if ee_id
is None or len(ee_id
) == 0:
410 raise N2VCBadArgumentsException(
411 message
="ee_id is mandatory", bad_args
=["ee_id"]
413 if artifact_path
is None or len(artifact_path
) == 0:
414 raise N2VCBadArgumentsException(
415 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
418 raise N2VCBadArgumentsException(
419 message
="db_dict is mandatory", bad_args
=["db_dict"]
427 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
429 "model: {}, application: {}, machine: {}".format(
430 model_name
, application_name
, machine_id
434 raise N2VCBadArgumentsException(
435 message
="ee_id={} is not a valid execution environment id".format(
441 # remove // in charm path
442 while artifact_path
.find("//") >= 0:
443 artifact_path
= artifact_path
.replace("//", "/")
446 if not self
.fs
.file_exists(artifact_path
):
447 msg
= "artifact path does not exist: {}".format(artifact_path
)
448 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
450 if artifact_path
.startswith("/"):
451 full_path
= self
.fs
.path
+ artifact_path
453 full_path
= self
.fs
.path
+ "/" + artifact_path
456 if vca_type
== "native_charm" and await libjuju
.check_application_exists(
457 model_name
, application_name
459 await libjuju
.add_unit(
460 application_name
=application_name
,
461 model_name
=model_name
,
462 machine_id
=machine_id
,
464 progress_timeout
=progress_timeout
,
465 total_timeout
=total_timeout
,
468 await libjuju
.deploy_charm(
469 model_name
=model_name
,
470 application_name
=application_name
,
472 machine_id
=machine_id
,
474 progress_timeout
=progress_timeout
,
475 total_timeout
=total_timeout
,
479 except JujuApplicationExists
as e
:
480 raise N2VCApplicationExists(
481 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
.message
)
483 except Exception as e
:
485 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
)
488 self
.log
.info("Configuration sw installed")
490 async def install_k8s_proxy_charm(
496 progress_timeout
: float = None,
497 total_timeout
: float = None,
502 Install a k8s proxy charm
504 :param charm_name: Name of the charm being deployed
505 :param namespace: collection of all the uuids related to the charm.
506 :param str artifact_path: where to locate the artifacts (parent folder) using
508 the final artifact path will be a combination of this artifact_path and
509 additional string from the config_dict (e.g. charm name)
510 :param dict db_dict: where to write into database when the status changes.
511 It contains a dict with
512 {collection: <str>, filter: {}, path: <str>},
513 e.g. {collection: "nsrs", filter:
514 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
515 :param: progress_timeout: Progress timeout
516 :param: total_timeout: Total timeout
517 :param config: Dictionary with additional configuration
518 :param vca_id: VCA ID
520 :returns ee_id: execution environment id.
523 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
524 charm_name
, artifact_path
, db_dict
527 libjuju
= await self
._get
_libjuju
(vca_id
)
529 if artifact_path
is None or len(artifact_path
) == 0:
530 raise N2VCBadArgumentsException(
531 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
534 raise N2VCBadArgumentsException(
535 message
="db_dict is mandatory", bad_args
=["db_dict"]
538 # remove // in charm path
539 while artifact_path
.find("//") >= 0:
540 artifact_path
= artifact_path
.replace("//", "/")
543 if not self
.fs
.file_exists(artifact_path
):
544 msg
= "artifact path does not exist: {}".format(artifact_path
)
545 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
547 if artifact_path
.startswith("/"):
548 full_path
= self
.fs
.path
+ artifact_path
550 full_path
= self
.fs
.path
+ "/" + artifact_path
552 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
553 model_name
= "{}-k8s".format(ns_id
)
554 if not await libjuju
.model_exists(model_name
):
555 await libjuju
.add_model(model_name
, libjuju
.vca_connection
.k8s_cloud
)
556 application_name
= self
._get
_application
_name
(namespace
)
559 await libjuju
.deploy_charm(
560 model_name
=model_name
,
561 application_name
=application_name
,
565 progress_timeout
=progress_timeout
,
566 total_timeout
=total_timeout
,
569 except Exception as e
:
570 raise N2VCException(message
="Error deploying charm: {}".format(e
))
572 self
.log
.info("K8s proxy charm installed")
573 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
574 model_name
=model_name
, application_name
=application_name
, machine_id
="k8s"
577 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
581 async def get_ee_ssh_public__key(
585 progress_timeout
: float = None,
586 total_timeout
: float = None,
590 Get Execution environment ssh public key
592 :param: ee_id: the id of the execution environment returned by
593 create_execution_environment or register_execution_environment
594 :param: db_dict: where to write into database when the status changes.
595 It contains a dict with
596 {collection: <str>, filter: {}, path: <str>},
597 e.g. {collection: "nsrs", filter:
598 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
599 :param: progress_timeout: Progress timeout
600 :param: total_timeout: Total timeout
601 :param vca_id: VCA ID
602 :returns: public key of the execution environment
603 For the case of juju proxy charm ssh-layered, it is the one
604 returned by 'get-ssh-public-key' primitive.
605 It raises a N2VC exception if fails
610 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
611 ).format(ee_id
, db_dict
)
613 libjuju
= await self
._get
_libjuju
(vca_id
)
616 if ee_id
is None or len(ee_id
) == 0:
617 raise N2VCBadArgumentsException(
618 message
="ee_id is mandatory", bad_args
=["ee_id"]
621 raise N2VCBadArgumentsException(
622 message
="db_dict is mandatory", bad_args
=["db_dict"]
630 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
632 "model: {}, application: {}, machine: {}".format(
633 model_name
, application_name
, machine_id
637 raise N2VCBadArgumentsException(
638 message
="ee_id={} is not a valid execution environment id".format(
644 # try to execute ssh layer primitives (if exist):
650 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
652 # execute action: generate-ssh-key
654 output
, _status
= await libjuju
.execute_action(
655 model_name
=model_name
,
656 application_name
=application_name
,
657 action_name
="generate-ssh-key",
659 progress_timeout
=progress_timeout
,
660 total_timeout
=total_timeout
,
662 except Exception as e
:
664 "Skipping exception while executing action generate-ssh-key: {}".format(
669 # execute action: get-ssh-public-key
671 output
, _status
= await libjuju
.execute_action(
672 model_name
=model_name
,
673 application_name
=application_name
,
674 action_name
="get-ssh-public-key",
676 progress_timeout
=progress_timeout
,
677 total_timeout
=total_timeout
,
679 except Exception as e
:
680 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
682 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
684 # return public key if exists
685 return output
["pubkey"] if "pubkey" in output
else output
687 async def get_metrics(
688 self
, model_name
: str, application_name
: str, vca_id
: str = None
691 Get metrics from application
693 :param: model_name: Model name
694 :param: application_name: Application name
695 :param: vca_id: VCA ID
697 :return: Dictionary with obtained metrics
699 libjuju
= await self
._get
_libjuju
(vca_id
)
700 return await libjuju
.get_metrics(model_name
, application_name
)
702 async def add_relation(
703 self
, provider
: RelationEndpoint
, requirer
: RelationEndpoint
706 Add relation between two charmed endpoints
708 :param: provider: Provider relation endpoint
709 :param: requirer: Requirer relation endpoint
711 self
.log
.debug(f
"adding new relation between {provider} and {requirer}")
712 cross_model_relation
= (
713 provider
.model_name
!= requirer
.model_name
714 or provider
.vca_id
!= requirer
.vca_id
717 if cross_model_relation
:
718 # Cross-model relation
719 provider_libjuju
= await self
._get
_libjuju
(provider
.vca_id
)
720 requirer_libjuju
= await self
._get
_libjuju
(requirer
.vca_id
)
721 offer
= await provider_libjuju
.offer(provider
)
723 saas_name
= await requirer_libjuju
.consume(
724 requirer
.model_name
, offer
, provider_libjuju
726 await requirer_libjuju
.add_relation(
727 requirer
.model_name
, requirer
.endpoint
, saas_name
731 vca_id
= provider
.vca_id
732 model
= provider
.model_name
733 libjuju
= await self
._get
_libjuju
(vca_id
)
734 # add juju relations between two applications
735 await libjuju
.add_relation(
737 endpoint_1
=provider
.endpoint
,
738 endpoint_2
=requirer
.endpoint
,
740 except Exception as e
:
741 message
= f
"Error adding relation between {provider} and {requirer}: {e}"
742 self
.log
.error(message
)
743 raise N2VCException(message
=message
)
745 async def remove_relation(self
):
747 self
.log
.info("Method not implemented yet")
748 raise MethodNotImplemented()
750 async def deregister_execution_environments(self
):
751 self
.log
.info("Method not implemented yet")
752 raise MethodNotImplemented()
754 async def delete_namespace(
757 db_dict
: dict = None,
758 total_timeout
: float = None,
762 Remove a network scenario and its execution environments
763 :param: namespace: [<nsi-id>].<ns-id>
764 :param: db_dict: where to write into database when the status changes.
765 It contains a dict with
766 {collection: <str>, filter: {}, path: <str>},
767 e.g. {collection: "nsrs", filter:
768 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
769 :param: total_timeout: Total timeout
770 :param: vca_id: VCA ID
772 self
.log
.info("Deleting namespace={}".format(namespace
))
773 will_not_delete
= False
774 if namespace
not in self
.delete_namespace_locks
:
775 self
.delete_namespace_locks
[namespace
] = asyncio
.Lock(loop
=self
.loop
)
776 delete_lock
= self
.delete_namespace_locks
[namespace
]
778 while delete_lock
.locked():
779 will_not_delete
= True
780 await asyncio
.sleep(0.1)
783 self
.log
.info("Namespace {} deleted by another worker.".format(namespace
))
787 async with delete_lock
:
788 libjuju
= await self
._get
_libjuju
(vca_id
)
791 if namespace
is None:
792 raise N2VCBadArgumentsException(
793 message
="namespace is mandatory", bad_args
=["namespace"]
802 ) = self
._get
_namespace
_components
(namespace
=namespace
)
803 if ns_id
is not None:
805 models
= await libjuju
.list_models(contains
=ns_id
)
807 await libjuju
.destroy_model(
808 model_name
=model
, total_timeout
=total_timeout
810 except Exception as e
:
811 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
813 message
="Error deleting namespace {} : {}".format(
818 raise N2VCBadArgumentsException(
819 message
="only ns_id is permitted to delete yet",
820 bad_args
=["namespace"],
822 except Exception as e
:
823 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
826 self
.delete_namespace_locks
.pop(namespace
)
827 self
.log
.info("Namespace {} deleted".format(namespace
))
829 async def delete_execution_environment(
832 db_dict
: dict = None,
833 total_timeout
: float = None,
834 scaling_in
: bool = False,
835 vca_type
: str = None,
839 Delete an execution environment
840 :param str ee_id: id of the execution environment to delete
841 :param dict db_dict: where to write into database when the status changes.
842 It contains a dict with
843 {collection: <str>, filter: {}, path: <str>},
844 e.g. {collection: "nsrs", filter:
845 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
846 :param: total_timeout: Total timeout
847 :param: scaling_in: Boolean to indicate if it is a scaling in operation
848 :param: vca_type: VCA type
849 :param: vca_id: VCA ID
851 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
852 libjuju
= await self
._get
_libjuju
(vca_id
)
856 raise N2VCBadArgumentsException(
857 message
="ee_id is mandatory", bad_args
=["ee_id"]
860 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
866 await libjuju
.destroy_model(
867 model_name
=model_name
, total_timeout
=total_timeout
869 elif vca_type
== "native_charm" and scaling_in
:
870 # destroy the unit in the application
871 await libjuju
.destroy_unit(
872 application_name
=application_name
,
873 model_name
=model_name
,
874 machine_id
=machine_id
,
875 total_timeout
=total_timeout
,
878 # destroy the application
879 await libjuju
.destroy_application(
880 model_name
=model_name
,
881 application_name
=application_name
,
882 total_timeout
=total_timeout
,
884 except Exception as e
:
887 "Error deleting execution environment {} (application {}) : {}"
888 ).format(ee_id
, application_name
, e
)
891 self
.log
.info("Execution environment {} deleted".format(ee_id
))
893 async def exec_primitive(
898 db_dict
: dict = None,
899 progress_timeout
: float = None,
900 total_timeout
: float = None,
902 vca_type
: str = None,
905 Execute a primitive in the execution environment
907 :param: ee_id: the one returned by create_execution_environment or
908 register_execution_environment
909 :param: primitive_name: must be one defined in the software. There is one
910 called 'config', where, for the proxy case, the 'credentials' of VM are
912 :param: params_dict: parameters of the action
913 :param: db_dict: where to write into database when the status changes.
914 It contains a dict with
915 {collection: <str>, filter: {}, path: <str>},
916 e.g. {collection: "nsrs", filter:
917 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
918 :param: progress_timeout: Progress timeout
919 :param: total_timeout: Total timeout
920 :param: vca_id: VCA ID
921 :param: vca_type: VCA type
922 :returns str: primitive result, if ok. It raises exceptions in case of fail
926 "Executing primitive: {} on ee: {}, params: {}".format(
927 primitive_name
, ee_id
, params_dict
930 libjuju
= await self
._get
_libjuju
(vca_id
)
933 if ee_id
is None or len(ee_id
) == 0:
934 raise N2VCBadArgumentsException(
935 message
="ee_id is mandatory", bad_args
=["ee_id"]
937 if primitive_name
is None or len(primitive_name
) == 0:
938 raise N2VCBadArgumentsException(
939 message
="action_name is mandatory", bad_args
=["action_name"]
941 if params_dict
is None:
949 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
950 # To run action on the leader unit in libjuju.execute_action function,
951 # machine_id must be set to None if vca_type is not native_charm
952 if vca_type
!= "native_charm":
955 raise N2VCBadArgumentsException(
956 message
="ee_id={} is not a valid execution environment id".format(
962 if primitive_name
== "config":
963 # Special case: config primitive
965 await libjuju
.configure_application(
966 model_name
=model_name
,
967 application_name
=application_name
,
970 actions
= await libjuju
.get_actions(
971 application_name
=application_name
, model_name
=model_name
974 "Application {} has these actions: {}".format(
975 application_name
, actions
978 if "verify-ssh-credentials" in actions
:
979 # execute verify-credentials
982 for _
in range(num_retries
):
984 self
.log
.debug("Executing action verify-ssh-credentials...")
985 output
, ok
= await libjuju
.execute_action(
986 model_name
=model_name
,
987 application_name
=application_name
,
988 action_name
="verify-ssh-credentials",
990 progress_timeout
=progress_timeout
,
991 total_timeout
=total_timeout
,
996 "Error executing verify-ssh-credentials: {}. Retrying..."
998 await asyncio
.sleep(retry_timeout
)
1001 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1003 except asyncio
.CancelledError
:
1007 "Error executing verify-ssh-credentials after {} retries. ".format(
1012 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
1015 self
.log
.debug(msg
=msg
)
1016 except Exception as e
:
1017 self
.log
.error("Error configuring juju application: {}".format(e
))
1018 raise N2VCExecutionException(
1019 message
="Error configuring application into ee={} : {}".format(
1022 primitive_name
=primitive_name
,
1027 output
, status
= await libjuju
.execute_action(
1028 model_name
=model_name
,
1029 application_name
=application_name
,
1030 action_name
=primitive_name
,
1032 machine_id
=machine_id
,
1033 progress_timeout
=progress_timeout
,
1034 total_timeout
=total_timeout
,
1037 if status
== "completed":
1040 if "output" in output
:
1041 raise Exception(f
'{status}: {output["output"]}')
1044 f
"{status}: No further information received from action"
1047 except Exception as e
:
1048 self
.log
.error(f
"Error executing primitive {primitive_name}: {e}")
1049 raise N2VCExecutionException(
1050 message
=f
"Error executing primitive {primitive_name} in ee={ee_id}: {e}",
1051 primitive_name
=primitive_name
,
1054 async def upgrade_charm(
1058 charm_id
: str = None,
1059 charm_type
: str = None,
1060 timeout
: float = None,
1062 """This method upgrade charms in VNFs
1065 ee_id: Execution environment id
1066 path: Local path to the charm
1068 charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm
1069 timeout: (Float) Timeout for the ns update operation
1072 The output of the update operation if status equals to "completed"
1075 self
.log
.info("Upgrading charm: {} on ee: {}".format(path
, ee_id
))
1076 libjuju
= await self
._get
_libjuju
(charm_id
)
1079 if ee_id
is None or len(ee_id
) == 0:
1080 raise N2VCBadArgumentsException(
1081 message
="ee_id is mandatory", bad_args
=["ee_id"]
1088 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
1091 raise N2VCBadArgumentsException(
1092 message
="ee_id={} is not a valid execution environment id".format(
1100 await libjuju
.upgrade_charm(
1101 application_name
=application_name
,
1103 model_name
=model_name
,
1104 total_timeout
=timeout
,
1107 return f
"Charm upgraded with application name {application_name}"
1109 except Exception as e
:
1110 self
.log
.error("Error upgrading charm {}: {}".format(path
, e
))
1112 raise N2VCException(
1113 message
="Error upgrading charm {} in ee={} : {}".format(path
, ee_id
, e
)
1116 async def disconnect(self
, vca_id
: str = None):
1120 :param: vca_id: VCA ID
1122 self
.log
.info("closing juju N2VC...")
1123 libjuju
= await self
._get
_libjuju
(vca_id
)
1125 await libjuju
.disconnect()
1126 except Exception as e
:
1127 raise N2VCConnectionException(
1128 message
="Error disconnecting controller: {}".format(e
),
1129 url
=libjuju
.vca_connection
.data
.endpoints
,
1133 ####################################################################################
1134 ################################### P R I V A T E ##################################
1135 ####################################################################################
1138 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1142 :param: vca_id: VCA ID
1143 If None, get a libjuju object with a Connection to the default VCA
1144 Else, geta libjuju object with a Connection to the specified VCA
1147 while self
.loading_libjuju
.locked():
1148 await asyncio
.sleep(0.1)
1149 if not self
.libjuju
:
1150 async with self
.loading_libjuju
:
1151 vca_connection
= await get_connection(self
._store
)
1152 self
.libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
)
1155 vca_connection
= await get_connection(self
._store
, vca_id
)
1156 return Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1158 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
1160 # write ee_id to database: _admin.deployed.VCA.x
1162 the_table
= db_dict
["collection"]
1163 the_filter
= db_dict
["filter"]
1164 the_path
= db_dict
["path"]
1165 if not the_path
[-1] == ".":
1166 the_path
= the_path
+ "."
1167 update_dict
= {the_path
+ "ee_id": ee_id
}
1168 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1171 q_filter
=the_filter
,
1172 update_dict
=update_dict
,
1175 except asyncio
.CancelledError
:
1177 except Exception as e
:
1178 self
.log
.error("Error writing ee_id to database: {}".format(e
))
1181 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
1183 Build an execution environment id form model, application and machine
1185 :param application_name:
1189 # id for the execution environment
1190 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1193 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1195 Get model, application and machine components from an execution environment id
1197 :return: model_name, application_name, machine_id
1200 return get_ee_id_components(ee_id
)
1203 def _find_charm_level(vnf_id
: str, vdu_id
: str) -> str:
1204 """Decides the charm level.
1206 vnf_id (str): VNF id
1207 vdu_id (str): VDU id
1210 charm_level (str): ns-level or vnf-level or vdu-level
1212 if vdu_id
and not vnf_id
:
1213 raise N2VCException(message
="If vdu-id exists, vnf-id should be provided.")
1214 if vnf_id
and vdu_id
:
1216 if vnf_id
and not vdu_id
:
1218 if not vnf_id
and not vdu_id
:
1222 def _generate_backward_compatible_application_name(
1223 vnf_id
: str, vdu_id
: str, vdu_count
: str
1225 """Generate backward compatible application name
1226 by limiting the app name to 50 characters.
1229 vnf_id (str): VNF ID
1230 vdu_id (str): VDU ID
1231 vdu_count (str): vdu-count-index
1234 application_name (str): generated application name
1237 if vnf_id
is None or len(vnf_id
) == 0:
1240 # Shorten the vnf_id to its last twelve characters
1241 vnf_id
= "vnf-" + vnf_id
[-12:]
1243 if vdu_id
is None or len(vdu_id
) == 0:
1246 # Shorten the vdu_id to its last twelve characters
1247 vdu_id
= "-vdu-" + vdu_id
[-12:]
1249 if vdu_count
is None or len(vdu_count
) == 0:
1252 vdu_count
= "-cnt-" + vdu_count
1254 # Generate a random suffix with 5 characters (the default size used by K8s)
1255 random_suffix
= generate_random_alfanum_string(size
=5)
1257 application_name
= "app-{}{}{}-{}".format(
1258 vnf_id
, vdu_id
, vdu_count
, random_suffix
1260 return application_name
1263 def _get_vca_record(search_key
: str, vca_records
: list, vdu_id
: str) -> dict:
1264 """Get the correct VCA record dict depending on the search key
1267 search_key (str): keyword to find the correct VCA record
1268 vca_records (list): All VCA records as list
1269 vdu_id (str): VDU ID
1272 vca_record (dict): Dictionary which includes the correct VCA record
1276 filter(lambda record
: record
[search_key
] == vdu_id
, vca_records
), {}
1280 def _generate_application_name(
1284 vnf_count
: str = None,
1286 vdu_count
: str = None,
1288 """Generate application name to make the relevant charm of VDU/KDU
1289 in the VNFD descriptor become clearly visible.
1290 Limiting the app name to 50 characters.
1293 charm_level (str): level of charm
1294 vnfrs (dict): vnf record dict
1295 vca_records (list): db_nsr["_admin"]["deployed"]["VCA"] as list
1296 vnf_count (str): vnf count index
1297 vdu_id (str): VDU ID
1298 vdu_count (str): vdu count index
1301 application_name (str): generated application name
1304 application_name
= ""
1305 if charm_level
== "ns-level":
1306 if len(vca_records
) != 1:
1307 raise N2VCException(message
="One VCA record is expected.")
1308 # Only one VCA record is expected if it's ns-level charm.
1309 # Shorten the charm name to its first 40 characters.
1310 charm_name
= vca_records
[0]["charm_name"][:40]
1312 raise N2VCException(message
="Charm name should be provided.")
1313 application_name
= charm_name
+ "-ns"
1315 elif charm_level
== "vnf-level":
1316 if len(vca_records
) < 1:
1317 raise N2VCException(message
="One or more VCA record is expected.")
1318 # If VNF is scaled, more than one VCA record may be included in vca_records
1319 # but ee_descriptor_id is same.
1320 # Shorten the ee_descriptor_id and member-vnf-index-ref
1321 # to first 12 characters.
1322 application_name
= (
1323 vca_records
[0]["ee_descriptor_id"][:12]
1327 + vnfrs
["member-vnf-index-ref"][:12]
1330 elif charm_level
== "vdu-level":
1331 if len(vca_records
) < 1:
1332 raise N2VCException(message
="One or more VCA record is expected.")
1334 # Charms are also used for deployments with Helm charts.
1335 # If deployment unit is a Helm chart/KDU,
1336 # vdu_profile_id and vdu_count will be empty string.
1337 if vdu_count
is None:
1340 # If vnf/vdu is scaled, more than one VCA record may be included in vca_records
1341 # but ee_descriptor_id is same.
1342 # Shorten the ee_descriptor_id, member-vnf-index-ref and vdu_profile_id
1343 # to first 12 characters.
1345 raise N2VCException(message
="vdu-id should be provided.")
1347 vca_record
= N2VCJujuConnector
._get
_vca
_record
(
1348 "vdu_id", vca_records
, vdu_id
1352 vca_record
= N2VCJujuConnector
._get
_vca
_record
(
1353 "kdu_name", vca_records
, vdu_id
1356 application_name
= (
1357 vca_record
["ee_descriptor_id"][:12]
1361 + vnfrs
["member-vnf-index-ref"][:12]
1369 return application_name
1371 def _get_vnf_count_and_record(
1372 self
, charm_level
: str, vnf_id_and_count
: str
1373 ) -> Tuple
[str, dict]:
1374 """Get the vnf count and VNF record depend on charm level
1378 vnf_id_and_count (str)
1381 (vnf_count (str), db_vnfr(dict)) as Tuple
1387 if charm_level
in ("vnf-level", "vdu-level"):
1388 vnf_id
= "-".join(vnf_id_and_count
.split("-")[:-1])
1389 vnf_count
= vnf_id_and_count
.split("-")[-1]
1390 db_vnfr
= self
.db
.get_one("vnfrs", {"_id": vnf_id
})
1392 # If the charm is ns level, it returns empty vnf_count and db_vnfr
1393 return vnf_count
, db_vnfr
1396 def _get_vca_records(charm_level
: str, db_nsr
: dict, db_vnfr
: dict) -> list:
1397 """Get the VCA records from db_nsr dict
1400 charm_level (str): level of charm
1401 db_nsr (dict): NS record from database
1402 db_vnfr (dict): VNF record from database
1405 vca_records (list): List of VCA record dictionaries
1409 if charm_level
== "ns-level":
1412 lambda vca_record
: vca_record
["target_element"] == "ns",
1413 db_nsr
["_admin"]["deployed"]["VCA"],
1416 elif charm_level
in ["vnf-level", "vdu-level"]:
1419 lambda vca_record
: vca_record
["member-vnf-index"]
1420 == db_vnfr
["member-vnf-index-ref"],
1421 db_nsr
["_admin"]["deployed"]["VCA"],
1427 def _get_application_name(self
, namespace
: str) -> str:
1428 """Build application name from namespace
1430 Application name structure:
1431 NS level: <charm-name>-ns
1432 VNF level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-vnf
1433 VDU level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-
1434 <vdu-profile-id>-z<vdu-ordinal-scale-number>-vdu
1436 Application naming for backward compatibility (old structure):
1437 NS level: app-<random_value>
1438 VNF level: app-vnf-<vnf-id>-z<ordinal-scale-number>-<random_value>
1439 VDU level: app-vnf-<vnf-id>-z<vnf-ordinal-scale-number>-vdu-
1440 <vdu-id>-cnt-<vdu-count>-z<vdu-ordinal-scale-number>-<random_value>
1446 application_name (str)
1449 # split namespace components
1456 ) = self
._get
_namespace
_components
(namespace
=namespace
)
1459 raise N2VCException(message
="ns-id should be provided.")
1461 charm_level
= self
._find
_charm
_level
(vnf_id_and_count
, vdu_id
)
1462 db_nsr
= self
.db
.get_one("nsrs", {"_id": ns_id
})
1463 vnf_count
, db_vnfr
= self
._get
_vnf
_count
_and
_record
(
1464 charm_level
, vnf_id_and_count
1466 vca_records
= self
._get
_vca
_records
(charm_level
, db_nsr
, db_vnfr
)
1468 if all("charm_name" in vca_record
.keys() for vca_record
in vca_records
):
1469 application_name
= self
._generate
_application
_name
(
1473 vnf_count
=vnf_count
,
1475 vdu_count
=vdu_count
,
1478 application_name
= self
._generate
_backward
_compatible
_application
_name
(
1479 vnf_id_and_count
, vdu_id
, vdu_count
1482 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1485 def _format_model_name(name
: str) -> str:
1486 """Format the name of the model.
1488 Model names may only contain lowercase letters, digits and hyphens
1491 return name
.replace("_", "-").replace(" ", "-").lower()
1494 def _format_app_name(name
: str) -> str:
1495 """Format the name of the application (in order to assure valid application name).
1497 Application names have restrictions (run juju deploy --help):
1498 - contains lowercase letters 'a'-'z'
1499 - contains numbers '0'-'9'
1500 - contains hyphens '-'
1501 - starts with a lowercase letter
1502 - not two or more consecutive hyphens
1503 - after a hyphen, not a group with all numbers
1506 def all_numbers(s
: str) -> bool:
1512 new_name
= name
.replace("_", "-")
1513 new_name
= new_name
.replace(" ", "-")
1514 new_name
= new_name
.lower()
1515 while new_name
.find("--") >= 0:
1516 new_name
= new_name
.replace("--", "-")
1517 groups
= new_name
.split("-")
1519 # find 'all numbers' groups and prefix them with a letter
1521 for i
in range(len(groups
)):
1523 if all_numbers(group
):
1529 if app_name
[0].isdigit():
1530 app_name
= "z" + app_name
1534 async def validate_vca(self
, vca_id
: str):
1536 Validate a VCA by connecting/disconnecting to/from it
1538 :param: vca_id: VCA ID
1540 vca_connection
= await get_connection(self
._store
, vca_id
=vca_id
)
1541 libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1542 controller
= await libjuju
.get_controller()
1543 await libjuju
.disconnect_controller(controller
)