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
__(
84 on_update_db
=on_update_db
,
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
)
92 self
.log
.info("Initializing N2VC juju connector...")
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")
100 async def get_status(
101 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
104 Get status from all juju models from a VCA
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.
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.
115 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
116 libjuju
= await self
._get
_libjuju
(vca_id
)
118 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
121 # model name is ns_id
123 if model_name
is None:
124 msg
= "Namespace {} not valid".format(namespace
)
126 raise N2VCBadArgumentsException(msg
, ["namespace"])
129 models
= await libjuju
.list_models(contains
=ns_id
)
132 status
[m
] = await libjuju
.get_model_status(m
)
135 return obj_to_yaml(status
)
137 return obj_to_dict(status
)
139 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
141 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
143 :param vcastatus: dict containing vcaStatus
144 :param: vca_id: VCA ID
149 libjuju
= await self
._get
_libjuju
(vca_id
)
150 for model_name
in vcastatus
:
151 # Adding executed actions
152 vcastatus
[model_name
][
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
][
159 ] = await libjuju
.get_actions(application
, model_name
)
160 # Adding application configs
161 vcastatus
[model_name
]["applications"][application
][
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
)))
167 async def create_execution_environment(
171 reuse_ee_id
: str = None,
172 progress_timeout
: float = None,
173 total_timeout
: float = None,
177 Create an Execution Environment. Returns when it is created or raises an
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
188 :param: progress_timeout: Progress timeout
189 :param: total_timeout: Total timeout
190 :param: vca_id: VCA ID
192 :returns: id of the new execution environment and credentials for it
193 (credentials can contains hostname, username, etc depending on underlying cloud)
197 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
198 namespace
, reuse_ee_id
201 libjuju
= await self
._get
_libjuju
(vca_id
)
205 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
215 ) = self
._get
_namespace
_components
(namespace
=namespace
)
216 # model name is ns_id
219 application_name
= self
._get
_application
_name
(namespace
=namespace
)
222 "model name: {}, application name: {}, machine_id: {}".format(
223 model_name
, application_name
, machine_id
227 # create or reuse a new juju machine
229 if not await libjuju
.model_exists(model_name
):
230 await libjuju
.add_model(
232 libjuju
.vca_connection
.lxd_cloud
,
234 machine
, new
= await libjuju
.create_machine(
235 model_name
=model_name
,
236 machine_id
=machine_id
,
238 progress_timeout
=progress_timeout
,
239 total_timeout
=total_timeout
,
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
),
247 self
.log
.debug("ee_id: {}".format(ee_id
))
250 # write ee_id in database
251 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
253 except Exception as e
:
254 message
= "Error creating machine on juju: {}".format(e
)
255 self
.log
.error(message
)
256 raise N2VCException(message
=message
)
258 # new machine credentials
260 "hostname": machine
.dns_name
,
264 "Execution environment created. ee_id: {}, credentials: {}".format(
269 return ee_id
, credentials
271 async def register_execution_environment(
276 progress_timeout
: float = None,
277 total_timeout
: float = None,
281 Register an existing execution environment at the VCA
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
294 :param: progress_timeout: Progress timeout
295 :param: total_timeout: Total timeout
296 :param: vca_id: VCA ID
298 :returns: id of the execution environment
301 "Registering execution environment. namespace={}, credentials={}".format(
302 namespace
, credentials
305 libjuju
= await self
._get
_libjuju
(vca_id
)
307 if credentials
is None:
308 raise N2VCBadArgumentsException(
309 message
="credentials are mandatory", bad_args
=["credentials"]
311 if credentials
.get("hostname"):
312 hostname
= credentials
["hostname"]
314 raise N2VCBadArgumentsException(
315 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
317 if credentials
.get("username"):
318 username
= credentials
["username"]
320 raise N2VCBadArgumentsException(
321 message
="username is mandatory", bad_args
=["credentials.username"]
323 if "private_key_path" in credentials
:
324 private_key_path
= credentials
["private_key_path"]
326 # if not passed as argument, use generated private key path
327 private_key_path
= self
.private_key_path
329 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
336 application_name
= self
._get
_application
_name
(namespace
=namespace
)
338 # register machine on juju
340 if not await libjuju
.model_exists(model_name
):
341 await libjuju
.add_model(
343 libjuju
.vca_connection
.lxd_cloud
,
345 machine_id
= await libjuju
.provision_machine(
346 model_name
=model_name
,
349 private_key_path
=private_key_path
,
351 progress_timeout
=progress_timeout
,
352 total_timeout
=total_timeout
,
354 except Exception as e
:
355 self
.log
.error("Error registering machine: {}".format(e
))
357 message
="Error registering machine on juju: {}".format(e
)
360 self
.log
.info("Machine registered: {}".format(machine_id
))
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
),
369 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
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(
381 progress_timeout
: float = None,
382 total_timeout
: float = None,
386 scaling_out
: bool = False,
387 vca_type
: str = None,
390 Install the software inside the execution environment identified by ee_id
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
396 the final artifact path will be a combination of this
397 artifact_path and additional string from the config_dict
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
415 "Installing configuration sw on ee_id: {}, "
416 "artifact path: {}, db_dict: {}"
417 ).format(ee_id
, artifact_path
, db_dict
)
419 libjuju
= await self
._get
_libjuju
(vca_id
)
422 if ee_id
is None or len(ee_id
) == 0:
423 raise N2VCBadArgumentsException(
424 message
="ee_id is mandatory", bad_args
=["ee_id"]
426 if artifact_path
is None or len(artifact_path
) == 0:
427 raise N2VCBadArgumentsException(
428 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
431 raise N2VCBadArgumentsException(
432 message
="db_dict is mandatory", bad_args
=["db_dict"]
440 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
442 "model: {}, application: {}, machine: {}".format(
443 model_name
, application_name
, machine_id
447 raise N2VCBadArgumentsException(
448 message
="ee_id={} is not a valid execution environment id".format(
454 # remove // in charm path
455 while artifact_path
.find("//") >= 0:
456 artifact_path
= artifact_path
.replace("//", "/")
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"])
463 if artifact_path
.startswith("/"):
464 full_path
= self
.fs
.path
+ artifact_path
466 full_path
= self
.fs
.path
+ "/" + artifact_path
469 if vca_type
== "native_charm" and await libjuju
.check_application_exists(
470 model_name
, application_name
472 await libjuju
.add_unit(
473 application_name
=application_name
,
474 model_name
=model_name
,
475 machine_id
=machine_id
,
477 progress_timeout
=progress_timeout
,
478 total_timeout
=total_timeout
,
481 await libjuju
.deploy_charm(
482 model_name
=model_name
,
483 application_name
=application_name
,
485 machine_id
=machine_id
,
487 progress_timeout
=progress_timeout
,
488 total_timeout
=total_timeout
,
492 except JujuApplicationExists
as e
:
493 raise N2VCApplicationExists(
494 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
.message
)
496 except Exception as e
:
498 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
)
501 self
.log
.info("Configuration sw installed")
503 async def install_k8s_proxy_charm(
509 progress_timeout
: float = None,
510 total_timeout
: float = None,
515 Install a k8s proxy charm
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
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
533 :returns ee_id: execution environment id.
536 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
537 charm_name
, artifact_path
, db_dict
540 libjuju
= await self
._get
_libjuju
(vca_id
)
542 if artifact_path
is None or len(artifact_path
) == 0:
543 raise N2VCBadArgumentsException(
544 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
547 raise N2VCBadArgumentsException(
548 message
="db_dict is mandatory", bad_args
=["db_dict"]
551 # remove // in charm path
552 while artifact_path
.find("//") >= 0:
553 artifact_path
= artifact_path
.replace("//", "/")
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"])
560 if artifact_path
.startswith("/"):
561 full_path
= self
.fs
.path
+ artifact_path
563 full_path
= self
.fs
.path
+ "/" + artifact_path
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(
570 libjuju
.vca_connection
.k8s_cloud
,
572 application_name
= self
._get
_application
_name
(namespace
)
575 await libjuju
.deploy_charm(
576 model_name
=model_name
,
577 application_name
=application_name
,
581 progress_timeout
=progress_timeout
,
582 total_timeout
=total_timeout
,
585 except Exception as e
:
586 raise N2VCException(message
="Error deploying charm: {}".format(e
))
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
,
595 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
599 async def get_ee_ssh_public__key(
603 progress_timeout
: float = None,
604 total_timeout
: float = None,
608 Get Execution environment ssh public key
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
628 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
629 ).format(ee_id
, db_dict
)
631 libjuju
= await self
._get
_libjuju
(vca_id
)
634 if ee_id
is None or len(ee_id
) == 0:
635 raise N2VCBadArgumentsException(
636 message
="ee_id is mandatory", bad_args
=["ee_id"]
639 raise N2VCBadArgumentsException(
640 message
="db_dict is mandatory", bad_args
=["db_dict"]
648 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
650 "model: {}, application: {}, machine: {}".format(
651 model_name
, application_name
, machine_id
655 raise N2VCBadArgumentsException(
656 message
="ee_id={} is not a valid execution environment id".format(
662 # try to execute ssh layer primitives (if exist):
668 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
670 # execute action: generate-ssh-key
672 output
, _status
= await libjuju
.execute_action(
673 model_name
=model_name
,
674 application_name
=application_name
,
675 action_name
="generate-ssh-key",
677 progress_timeout
=progress_timeout
,
678 total_timeout
=total_timeout
,
680 except Exception as e
:
682 "Skipping exception while executing action generate-ssh-key: {}".format(
687 # execute action: get-ssh-public-key
689 output
, _status
= await libjuju
.execute_action(
690 model_name
=model_name
,
691 application_name
=application_name
,
692 action_name
="get-ssh-public-key",
694 progress_timeout
=progress_timeout
,
695 total_timeout
=total_timeout
,
697 except Exception as e
:
698 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
700 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
702 # return public key if exists
703 return output
["pubkey"] if "pubkey" in output
else output
705 async def get_metrics(
706 self
, model_name
: str, application_name
: str, vca_id
: str = None
709 Get metrics from application
711 :param: model_name: Model name
712 :param: application_name: Application name
713 :param: vca_id: VCA ID
715 :return: Dictionary with obtained metrics
717 libjuju
= await self
._get
_libjuju
(vca_id
)
718 return await libjuju
.get_metrics(model_name
, application_name
)
720 async def add_relation(
722 provider
: RelationEndpoint
,
723 requirer
: RelationEndpoint
,
726 Add relation between two charmed endpoints
728 :param: provider: Provider relation endpoint
729 :param: requirer: Requirer relation endpoint
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
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
)
743 saas_name
= await requirer_libjuju
.consume(
744 requirer
.model_name
, offer
, provider_libjuju
746 await requirer_libjuju
.add_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(
759 endpoint_1
=provider
.endpoint
,
760 endpoint_2
=requirer
.endpoint
,
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
)
767 async def remove_relation(self
):
769 self
.log
.info("Method not implemented yet")
770 raise MethodNotImplemented()
772 async def deregister_execution_environments(self
):
773 self
.log
.info("Method not implemented yet")
774 raise MethodNotImplemented()
776 async def delete_namespace(
779 db_dict
: dict = None,
780 total_timeout
: float = None,
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
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
]
800 while delete_lock
.locked():
801 will_not_delete
= True
802 await asyncio
.sleep(0.1)
805 self
.log
.info("Namespace {} deleted by another worker.".format(namespace
))
809 async with delete_lock
:
810 libjuju
= await self
._get
_libjuju
(vca_id
)
813 if namespace
is None:
814 raise N2VCBadArgumentsException(
815 message
="namespace is mandatory", bad_args
=["namespace"]
824 ) = self
._get
_namespace
_components
(namespace
=namespace
)
825 if ns_id
is not None:
827 models
= await libjuju
.list_models(contains
=ns_id
)
829 await libjuju
.destroy_model(
830 model_name
=model
, total_timeout
=total_timeout
832 except Exception as e
:
833 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
835 message
="Error deleting namespace {} : {}".format(
840 raise N2VCBadArgumentsException(
841 message
="only ns_id is permitted to delete yet",
842 bad_args
=["namespace"],
844 except Exception as e
:
845 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
848 self
.delete_namespace_locks
.pop(namespace
)
849 self
.log
.info("Namespace {} deleted".format(namespace
))
851 async def delete_execution_environment(
854 db_dict
: dict = None,
855 total_timeout
: float = None,
856 scaling_in
: bool = False,
857 vca_type
: str = None,
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
873 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
874 libjuju
= await self
._get
_libjuju
(vca_id
)
878 raise N2VCBadArgumentsException(
879 message
="ee_id is mandatory", bad_args
=["ee_id"]
882 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
888 await libjuju
.destroy_model(
889 model_name
=model_name
,
890 total_timeout
=total_timeout
,
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
,
901 # destroy the application
902 await libjuju
.destroy_application(
903 model_name
=model_name
,
904 application_name
=application_name
,
905 total_timeout
=total_timeout
,
907 except Exception as e
:
910 "Error deleting execution environment {} (application {}) : {}"
911 ).format(ee_id
, application_name
, e
)
914 self
.log
.info("Execution environment {} deleted".format(ee_id
))
916 async def exec_primitive(
921 db_dict
: dict = None,
922 progress_timeout
: float = None,
923 total_timeout
: float = None,
925 vca_type
: str = None,
928 Execute a primitive in the execution environment
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
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
949 "Executing primitive: {} on ee: {}, params: {}".format(
950 primitive_name
, ee_id
, params_dict
953 libjuju
= await self
._get
_libjuju
(vca_id
)
956 if ee_id
is None or len(ee_id
) == 0:
957 raise N2VCBadArgumentsException(
958 message
="ee_id is mandatory", bad_args
=["ee_id"]
960 if primitive_name
is None or len(primitive_name
) == 0:
961 raise N2VCBadArgumentsException(
962 message
="action_name is mandatory", bad_args
=["action_name"]
964 if params_dict
is None:
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":
978 raise N2VCBadArgumentsException(
979 message
="ee_id={} is not a valid execution environment id".format(
985 if primitive_name
== "config":
986 # Special case: config primitive
988 await libjuju
.configure_application(
989 model_name
=model_name
,
990 application_name
=application_name
,
993 actions
= await libjuju
.get_actions(
994 application_name
=application_name
,
995 model_name
=model_name
,
998 "Application {} has these actions: {}".format(
999 application_name
, actions
1002 if "verify-ssh-credentials" in actions
:
1003 # execute verify-credentials
1005 retry_timeout
= 15.0
1006 for _
in range(num_retries
):
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",
1014 progress_timeout
=progress_timeout
,
1015 total_timeout
=total_timeout
,
1020 "Error executing verify-ssh-credentials: {}. Retrying..."
1022 await asyncio
.sleep(retry_timeout
)
1025 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1027 except asyncio
.CancelledError
:
1031 "Error executing verify-ssh-credentials after {} retries. ".format(
1036 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
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(
1046 primitive_name
=primitive_name
,
1051 output
, status
= await libjuju
.execute_action(
1052 model_name
=model_name
,
1053 application_name
=application_name
,
1054 action_name
=primitive_name
,
1056 machine_id
=machine_id
,
1057 progress_timeout
=progress_timeout
,
1058 total_timeout
=total_timeout
,
1061 if status
== "completed":
1064 if "output" in output
:
1065 raise Exception(f
'{status}: {output["output"]}')
1068 f
"{status}: No further information received from action"
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
,
1078 async def upgrade_charm(
1082 charm_id
: str = None,
1083 charm_type
: str = None,
1084 timeout
: float = None,
1086 """This method upgrade charms in VNFs
1089 ee_id: Execution environment id
1090 path: Local path to the charm
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
1096 The output of the update operation if status equals to "completed"
1099 self
.log
.info("Upgrading charm: {} on ee: {}".format(path
, ee_id
))
1100 libjuju
= await self
._get
_libjuju
(charm_id
)
1103 if ee_id
is None or len(ee_id
) == 0:
1104 raise N2VCBadArgumentsException(
1105 message
="ee_id is mandatory", bad_args
=["ee_id"]
1112 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
1115 raise N2VCBadArgumentsException(
1116 message
="ee_id={} is not a valid execution environment id".format(
1123 await libjuju
.upgrade_charm(
1124 application_name
=application_name
,
1126 model_name
=model_name
,
1127 total_timeout
=timeout
,
1130 return f
"Charm upgraded with application name {application_name}"
1132 except Exception as e
:
1133 self
.log
.error("Error upgrading charm {}: {}".format(path
, e
))
1135 raise N2VCException(
1136 message
="Error upgrading charm {} in ee={} : {}".format(path
, ee_id
, e
)
1139 async def disconnect(self
, vca_id
: str = None):
1143 :param: vca_id: VCA ID
1145 self
.log
.info("closing juju N2VC...")
1146 libjuju
= await self
._get
_libjuju
(vca_id
)
1148 await libjuju
.disconnect()
1149 except Exception as e
:
1150 raise N2VCConnectionException(
1151 message
="Error disconnecting controller: {}".format(e
),
1152 url
=libjuju
.vca_connection
.data
.endpoints
,
1156 ####################################################################################
1157 ################################### P R I V A T E ##################################
1158 ####################################################################################
1161 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1165 :param: vca_id: VCA ID
1166 If None, get a libjuju object with a Connection to the default VCA
1167 Else, geta libjuju object with a Connection to the specified VCA
1170 while self
.loading_libjuju
.locked():
1171 await asyncio
.sleep(0.1)
1172 if not self
.libjuju
:
1173 async with self
.loading_libjuju
:
1174 vca_connection
= await get_connection(self
._store
)
1175 self
.libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
)
1178 vca_connection
= await get_connection(self
._store
, vca_id
)
1186 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
1187 # write ee_id to database: _admin.deployed.VCA.x
1189 the_table
= db_dict
["collection"]
1190 the_filter
= db_dict
["filter"]
1191 the_path
= db_dict
["path"]
1192 if not the_path
[-1] == ".":
1193 the_path
= the_path
+ "."
1194 update_dict
= {the_path
+ "ee_id": ee_id
}
1195 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1198 q_filter
=the_filter
,
1199 update_dict
=update_dict
,
1202 except asyncio
.CancelledError
:
1204 except Exception as e
:
1205 self
.log
.error("Error writing ee_id to database: {}".format(e
))
1208 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
1210 Build an execution environment id form model, application and machine
1212 :param application_name:
1216 # id for the execution environment
1217 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1220 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1222 Get model, application and machine components from an execution environment id
1224 :return: model_name, application_name, machine_id
1227 return get_ee_id_components(ee_id
)
1230 def _find_charm_level(vnf_id
: str, vdu_id
: str) -> str:
1231 """Decides the charm level.
1233 vnf_id (str): VNF id
1234 vdu_id (str): VDU id
1237 charm_level (str): ns-level or vnf-level or vdu-level
1239 if vdu_id
and not vnf_id
:
1240 raise N2VCException(message
="If vdu-id exists, vnf-id should be provided.")
1241 if vnf_id
and vdu_id
:
1243 if vnf_id
and not vdu_id
:
1245 if not vnf_id
and not vdu_id
:
1249 def _generate_backward_compatible_application_name(
1250 vnf_id
: str, vdu_id
: str, vdu_count
: str
1252 """Generate backward compatible application name
1253 by limiting the app name to 50 characters.
1256 vnf_id (str): VNF ID
1257 vdu_id (str): VDU ID
1258 vdu_count (str): vdu-count-index
1261 application_name (str): generated application name
1264 if vnf_id
is None or len(vnf_id
) == 0:
1267 # Shorten the vnf_id to its last twelve characters
1268 vnf_id
= "vnf-" + vnf_id
[-12:]
1270 if vdu_id
is None or len(vdu_id
) == 0:
1273 # Shorten the vdu_id to its last twelve characters
1274 vdu_id
= "-vdu-" + vdu_id
[-12:]
1276 if vdu_count
is None or len(vdu_count
) == 0:
1279 vdu_count
= "-cnt-" + vdu_count
1281 # Generate a random suffix with 5 characters (the default size used by K8s)
1282 random_suffix
= generate_random_alfanum_string(size
=5)
1284 application_name
= "app-{}{}{}-{}".format(
1285 vnf_id
, vdu_id
, vdu_count
, random_suffix
1287 return application_name
1290 def _get_vca_record(search_key
: str, vca_records
: list, vdu_id
: str) -> dict:
1291 """Get the correct VCA record dict depending on the search key
1294 search_key (str): keyword to find the correct VCA record
1295 vca_records (list): All VCA records as list
1296 vdu_id (str): VDU ID
1299 vca_record (dict): Dictionary which includes the correct VCA record
1303 filter(lambda record
: record
[search_key
] == vdu_id
, vca_records
), {}
1307 def _generate_application_name(
1311 vnf_count
: str = None,
1313 vdu_count
: str = None,
1315 """Generate application name to make the relevant charm of VDU/KDU
1316 in the VNFD descriptor become clearly visible.
1317 Limiting the app name to 50 characters.
1320 charm_level (str): level of charm
1321 vnfrs (dict): vnf record dict
1322 vca_records (list): db_nsr["_admin"]["deployed"]["VCA"] as list
1323 vnf_count (str): vnf count index
1324 vdu_id (str): VDU ID
1325 vdu_count (str): vdu count index
1328 application_name (str): generated application name
1331 application_name
= ""
1332 if charm_level
== "ns-level":
1333 if len(vca_records
) != 1:
1334 raise N2VCException(message
="One VCA record is expected.")
1335 # Only one VCA record is expected if it's ns-level charm.
1336 # Shorten the charm name to its first 40 characters.
1337 charm_name
= vca_records
[0]["charm_name"][:40]
1339 raise N2VCException(message
="Charm name should be provided.")
1340 application_name
= charm_name
+ "-ns"
1342 elif charm_level
== "vnf-level":
1343 if len(vca_records
) < 1:
1344 raise N2VCException(message
="One or more VCA record is expected.")
1345 # If VNF is scaled, more than one VCA record may be included in vca_records
1346 # but ee_descriptor_id is same.
1347 # Shorten the ee_descriptor_id and member-vnf-index-ref
1348 # to first 12 characters.
1349 application_name
= (
1350 vca_records
[0]["ee_descriptor_id"][:12]
1354 + vnfrs
["member-vnf-index-ref"][:12]
1357 elif charm_level
== "vdu-level":
1358 if len(vca_records
) < 1:
1359 raise N2VCException(message
="One or more VCA record is expected.")
1361 # Charms are also used for deployments with Helm charts.
1362 # If deployment unit is a Helm chart/KDU,
1363 # vdu_profile_id and vdu_count will be empty string.
1364 if vdu_count
is None:
1367 # If vnf/vdu is scaled, more than one VCA record may be included in vca_records
1368 # but ee_descriptor_id is same.
1369 # Shorten the ee_descriptor_id, member-vnf-index-ref and vdu_profile_id
1370 # to first 12 characters.
1372 raise N2VCException(message
="vdu-id should be provided.")
1374 vca_record
= N2VCJujuConnector
._get
_vca
_record
(
1375 "vdu_id", vca_records
, vdu_id
1379 vca_record
= N2VCJujuConnector
._get
_vca
_record
(
1380 "kdu_name", vca_records
, vdu_id
1383 application_name
= (
1384 vca_record
["ee_descriptor_id"][:12]
1388 + vnfrs
["member-vnf-index-ref"][:12]
1396 return application_name
1398 def _get_vnf_count_and_record(
1399 self
, charm_level
: str, vnf_id_and_count
: str
1400 ) -> Tuple
[str, dict]:
1401 """Get the vnf count and VNF record depend on charm level
1405 vnf_id_and_count (str)
1408 (vnf_count (str), db_vnfr(dict)) as Tuple
1414 if charm_level
in ("vnf-level", "vdu-level"):
1415 vnf_id
= "-".join(vnf_id_and_count
.split("-")[:-1])
1416 vnf_count
= vnf_id_and_count
.split("-")[-1]
1417 db_vnfr
= self
.db
.get_one("vnfrs", {"_id": vnf_id
})
1419 # If the charm is ns level, it returns empty vnf_count and db_vnfr
1420 return vnf_count
, db_vnfr
1423 def _get_vca_records(charm_level
: str, db_nsr
: dict, db_vnfr
: dict) -> list:
1424 """Get the VCA records from db_nsr dict
1427 charm_level (str): level of charm
1428 db_nsr (dict): NS record from database
1429 db_vnfr (dict): VNF record from database
1432 vca_records (list): List of VCA record dictionaries
1436 if charm_level
== "ns-level":
1439 lambda vca_record
: vca_record
["target_element"] == "ns",
1440 db_nsr
["_admin"]["deployed"]["VCA"],
1443 elif charm_level
in ["vnf-level", "vdu-level"]:
1446 lambda vca_record
: vca_record
["member-vnf-index"]
1447 == db_vnfr
["member-vnf-index-ref"],
1448 db_nsr
["_admin"]["deployed"]["VCA"],
1454 def _get_application_name(self
, namespace
: str) -> str:
1455 """Build application name from namespace
1457 Application name structure:
1458 NS level: <charm-name>-ns
1459 VNF level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-vnf
1460 VDU level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-
1461 <vdu-profile-id>-z<vdu-ordinal-scale-number>-vdu
1463 Application naming for backward compatibility (old structure):
1464 NS level: app-<random_value>
1465 VNF level: app-vnf-<vnf-id>-z<ordinal-scale-number>-<random_value>
1466 VDU level: app-vnf-<vnf-id>-z<vnf-ordinal-scale-number>-vdu-
1467 <vdu-id>-cnt-<vdu-count>-z<vdu-ordinal-scale-number>-<random_value>
1473 application_name (str)
1476 # split namespace components
1483 ) = self
._get
_namespace
_components
(namespace
=namespace
)
1486 raise N2VCException(message
="ns-id should be provided.")
1488 charm_level
= self
._find
_charm
_level
(vnf_id_and_count
, vdu_id
)
1489 db_nsr
= self
.db
.get_one("nsrs", {"_id": ns_id
})
1490 vnf_count
, db_vnfr
= self
._get
_vnf
_count
_and
_record
(
1491 charm_level
, vnf_id_and_count
1493 vca_records
= self
._get
_vca
_records
(charm_level
, db_nsr
, db_vnfr
)
1495 if all("charm_name" in vca_record
.keys() for vca_record
in vca_records
):
1496 application_name
= self
._generate
_application
_name
(
1500 vnf_count
=vnf_count
,
1502 vdu_count
=vdu_count
,
1505 application_name
= self
._generate
_backward
_compatible
_application
_name
(
1506 vnf_id_and_count
, vdu_id
, vdu_count
1509 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1512 def _format_model_name(name
: str) -> str:
1513 """Format the name of the model.
1515 Model names may only contain lowercase letters, digits and hyphens
1518 return name
.replace("_", "-").replace(" ", "-").lower()
1521 def _format_app_name(name
: str) -> str:
1522 """Format the name of the application (in order to assure valid application name).
1524 Application names have restrictions (run juju deploy --help):
1525 - contains lowercase letters 'a'-'z'
1526 - contains numbers '0'-'9'
1527 - contains hyphens '-'
1528 - starts with a lowercase letter
1529 - not two or more consecutive hyphens
1530 - after a hyphen, not a group with all numbers
1533 def all_numbers(s
: str) -> bool:
1539 new_name
= name
.replace("_", "-")
1540 new_name
= new_name
.replace(" ", "-")
1541 new_name
= new_name
.lower()
1542 while new_name
.find("--") >= 0:
1543 new_name
= new_name
.replace("--", "-")
1544 groups
= new_name
.split("-")
1546 # find 'all numbers' groups and prefix them with a letter
1548 for i
in range(len(groups
)):
1550 if all_numbers(group
):
1556 if app_name
[0].isdigit():
1557 app_name
= "z" + app_name
1561 async def validate_vca(self
, vca_id
: str):
1563 Validate a VCA by connecting/disconnecting to/from it
1565 :param: vca_id: VCA ID
1567 vca_connection
= await get_connection(self
._store
, vca_id
=vca_id
)
1568 libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1569 controller
= await libjuju
.get_controller()
1570 await libjuju
.disconnect_controller(controller
)