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
, 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
48 class N2VCJujuConnector(N2VCConnector
):
51 ####################################################################################
52 ################################### P U B L I C ####################################
53 ####################################################################################
56 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
69 :param: db: Database object from osm_common
70 :param: fs: Filesystem object from osm_common
72 :param: on_update_db: Callback function to be called for updating the database.
75 # parent class constructor
76 N2VCConnector
.__init
__(self
, db
=db
, fs
=fs
, log
=log
, on_update_db
=on_update_db
)
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
)
83 self
.log
.info("Initializing N2VC juju connector...")
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")
92 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
95 Get status from all juju models from a VCA
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.
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.
106 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
107 libjuju
= await self
._get
_libjuju
(vca_id
)
109 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
112 # model name is ns_id
114 if model_name
is None:
115 msg
= "Namespace {} not valid".format(namespace
)
117 raise N2VCBadArgumentsException(msg
, ["namespace"])
120 models
= await libjuju
.list_models(contains
=ns_id
)
123 status
[m
] = await libjuju
.get_model_status(m
)
126 return obj_to_yaml(status
)
128 return obj_to_dict(status
)
130 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
132 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
134 :param vcastatus: dict containing vcaStatus
135 :param: vca_id: VCA ID
140 libjuju
= await self
._get
_libjuju
(vca_id
)
141 for model_name
in vcastatus
:
142 # Adding executed actions
143 vcastatus
[model_name
][
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
][
150 ] = await libjuju
.get_actions(application
, model_name
)
151 # Adding application configs
152 vcastatus
[model_name
]["applications"][application
][
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
)))
158 async def create_execution_environment(
162 reuse_ee_id
: str = None,
163 progress_timeout
: float = None,
164 total_timeout
: float = None,
168 Create an Execution Environment. Returns when it is created or raises an
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
179 :param: progress_timeout: Progress timeout
180 :param: total_timeout: Total timeout
181 :param: vca_id: VCA ID
183 :returns: id of the new execution environment and credentials for it
184 (credentials can contains hostname, username, etc depending on underlying cloud)
188 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
189 namespace
, reuse_ee_id
192 libjuju
= await self
._get
_libjuju
(vca_id
)
196 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
206 ) = self
._get
_namespace
_components
(namespace
=namespace
)
207 # model name is ns_id
210 application_name
= self
._get
_application
_name
(namespace
=namespace
)
213 "model name: {}, application name: {}, machine_id: {}".format(
214 model_name
, application_name
, machine_id
218 # create or reuse a new juju machine
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
,
226 progress_timeout
=progress_timeout
,
227 total_timeout
=total_timeout
,
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
),
235 self
.log
.debug("ee_id: {}".format(ee_id
))
238 # write ee_id in database
239 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
241 except Exception as e
:
242 message
= "Error creating machine on juju: {}".format(e
)
243 self
.log
.error(message
)
244 raise N2VCException(message
=message
)
246 # new machine credentials
247 credentials
= {"hostname": machine
.dns_name
}
250 "Execution environment created. ee_id: {}, credentials: {}".format(
255 return ee_id
, credentials
257 async def register_execution_environment(
262 progress_timeout
: float = None,
263 total_timeout
: float = None,
267 Register an existing execution environment at the VCA
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
280 :param: progress_timeout: Progress timeout
281 :param: total_timeout: Total timeout
282 :param: vca_id: VCA ID
284 :returns: id of the execution environment
287 "Registering execution environment. namespace={}, credentials={}".format(
288 namespace
, credentials
291 libjuju
= await self
._get
_libjuju
(vca_id
)
293 if credentials
is None:
294 raise N2VCBadArgumentsException(
295 message
="credentials are mandatory", bad_args
=["credentials"]
297 if credentials
.get("hostname"):
298 hostname
= credentials
["hostname"]
300 raise N2VCBadArgumentsException(
301 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
303 if credentials
.get("username"):
304 username
= credentials
["username"]
306 raise N2VCBadArgumentsException(
307 message
="username is mandatory", bad_args
=["credentials.username"]
309 if "private_key_path" in credentials
:
310 private_key_path
= credentials
["private_key_path"]
312 # if not passed as argument, use generated private key path
313 private_key_path
= self
.private_key_path
315 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
322 application_name
= self
._get
_application
_name
(namespace
=namespace
)
324 # register machine on juju
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
,
332 private_key_path
=private_key_path
,
334 progress_timeout
=progress_timeout
,
335 total_timeout
=total_timeout
,
337 except Exception as e
:
338 self
.log
.error("Error registering machine: {}".format(e
))
340 message
="Error registering machine on juju: {}".format(e
)
343 self
.log
.info("Machine registered: {}".format(machine_id
))
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
),
352 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
356 # In case of native_charm is being deployed, if JujuApplicationExists error happens
357 # it will try to add_unit
361 retry_exceptions
=(N2VCApplicationExists
,),
363 callback
=retry_callback
,
365 async def install_configuration_sw(
370 progress_timeout
: float = None,
371 total_timeout
: float = None,
375 scaling_out
: bool = False,
376 vca_type
: str = None,
379 Install the software inside the execution environment identified by ee_id
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
385 the final artifact path will be a combination of this
386 artifact_path and additional string from the config_dict
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
404 "Installing configuration sw on ee_id: {}, "
405 "artifact path: {}, db_dict: {}"
406 ).format(ee_id
, artifact_path
, db_dict
)
408 libjuju
= await self
._get
_libjuju
(vca_id
)
411 if ee_id
is None or len(ee_id
) == 0:
412 raise N2VCBadArgumentsException(
413 message
="ee_id is mandatory", bad_args
=["ee_id"]
415 if artifact_path
is None or len(artifact_path
) == 0:
416 raise N2VCBadArgumentsException(
417 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
420 raise N2VCBadArgumentsException(
421 message
="db_dict is mandatory", bad_args
=["db_dict"]
429 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
431 "model: {}, application: {}, machine: {}".format(
432 model_name
, application_name
, machine_id
436 raise N2VCBadArgumentsException(
437 message
="ee_id={} is not a valid execution environment id".format(
443 # remove // in charm path
444 while artifact_path
.find("//") >= 0:
445 artifact_path
= artifact_path
.replace("//", "/")
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"])
452 if artifact_path
.startswith("/"):
453 full_path
= self
.fs
.path
+ artifact_path
455 full_path
= self
.fs
.path
+ "/" + artifact_path
458 if vca_type
== "native_charm" and await libjuju
.check_application_exists(
459 model_name
, application_name
461 await libjuju
.add_unit(
462 application_name
=application_name
,
463 model_name
=model_name
,
464 machine_id
=machine_id
,
466 progress_timeout
=progress_timeout
,
467 total_timeout
=total_timeout
,
470 await libjuju
.deploy_charm(
471 model_name
=model_name
,
472 application_name
=application_name
,
474 machine_id
=machine_id
,
476 progress_timeout
=progress_timeout
,
477 total_timeout
=total_timeout
,
481 except JujuApplicationExists
as e
:
482 raise N2VCApplicationExists(
483 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
.message
)
485 except Exception as e
:
487 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
)
490 self
.log
.info("Configuration sw installed")
492 async def install_k8s_proxy_charm(
498 progress_timeout
: float = None,
499 total_timeout
: float = None,
504 Install a k8s proxy charm
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
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
522 :returns ee_id: execution environment id.
525 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
526 charm_name
, artifact_path
, db_dict
529 libjuju
= await self
._get
_libjuju
(vca_id
)
531 if artifact_path
is None or len(artifact_path
) == 0:
532 raise N2VCBadArgumentsException(
533 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
536 raise N2VCBadArgumentsException(
537 message
="db_dict is mandatory", bad_args
=["db_dict"]
540 # remove // in charm path
541 while artifact_path
.find("//") >= 0:
542 artifact_path
= artifact_path
.replace("//", "/")
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"])
549 if artifact_path
.startswith("/"):
550 full_path
= self
.fs
.path
+ artifact_path
552 full_path
= self
.fs
.path
+ "/" + artifact_path
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
)
561 await libjuju
.deploy_charm(
562 model_name
=model_name
,
563 application_name
=application_name
,
567 progress_timeout
=progress_timeout
,
568 total_timeout
=total_timeout
,
571 except Exception as e
:
572 raise N2VCException(message
="Error deploying charm: {}".format(e
))
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"
579 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
583 async def get_ee_ssh_public__key(
587 progress_timeout
: float = None,
588 total_timeout
: float = None,
592 Get Execution environment ssh public key
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
612 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
613 ).format(ee_id
, db_dict
)
615 libjuju
= await self
._get
_libjuju
(vca_id
)
618 if ee_id
is None or len(ee_id
) == 0:
619 raise N2VCBadArgumentsException(
620 message
="ee_id is mandatory", bad_args
=["ee_id"]
623 raise N2VCBadArgumentsException(
624 message
="db_dict is mandatory", bad_args
=["db_dict"]
632 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
634 "model: {}, application: {}, machine: {}".format(
635 model_name
, application_name
, machine_id
639 raise N2VCBadArgumentsException(
640 message
="ee_id={} is not a valid execution environment id".format(
646 # try to execute ssh layer primitives (if exist):
652 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
654 # execute action: generate-ssh-key
656 output
, _status
= await libjuju
.execute_action(
657 model_name
=model_name
,
658 application_name
=application_name
,
659 action_name
="generate-ssh-key",
661 progress_timeout
=progress_timeout
,
662 total_timeout
=total_timeout
,
664 except Exception as e
:
666 "Skipping exception while executing action generate-ssh-key: {}".format(
671 # execute action: get-ssh-public-key
673 output
, _status
= await libjuju
.execute_action(
674 model_name
=model_name
,
675 application_name
=application_name
,
676 action_name
="get-ssh-public-key",
678 progress_timeout
=progress_timeout
,
679 total_timeout
=total_timeout
,
681 except Exception as e
:
682 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
684 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
686 # return public key if exists
687 return output
["pubkey"] if "pubkey" in output
else output
689 async def get_metrics(
690 self
, model_name
: str, application_name
: str, vca_id
: str = None
693 Get metrics from application
695 :param: model_name: Model name
696 :param: application_name: Application name
697 :param: vca_id: VCA ID
699 :return: Dictionary with obtained metrics
701 libjuju
= await self
._get
_libjuju
(vca_id
)
702 return await libjuju
.get_metrics(model_name
, application_name
)
704 async def add_relation(
705 self
, provider
: RelationEndpoint
, requirer
: RelationEndpoint
708 Add relation between two charmed endpoints
710 :param: provider: Provider relation endpoint
711 :param: requirer: Requirer relation endpoint
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
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
)
725 saas_name
= await requirer_libjuju
.consume(
726 requirer
.model_name
, offer
, provider_libjuju
728 await requirer_libjuju
.add_relation(
729 requirer
.model_name
, requirer
.endpoint
, saas_name
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(
739 endpoint_1
=provider
.endpoint
,
740 endpoint_2
=requirer
.endpoint
,
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
)
747 async def remove_relation(self
):
749 self
.log
.info("Method not implemented yet")
750 raise MethodNotImplemented()
752 async def deregister_execution_environments(self
):
753 self
.log
.info("Method not implemented yet")
754 raise MethodNotImplemented()
756 async def delete_namespace(
759 db_dict
: dict = None,
760 total_timeout
: float = None,
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
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
]
780 while delete_lock
.locked():
781 will_not_delete
= True
782 await asyncio
.sleep(0.1)
785 self
.log
.info("Namespace {} deleted by another worker.".format(namespace
))
789 async with delete_lock
:
790 libjuju
= await self
._get
_libjuju
(vca_id
)
793 if namespace
is None:
794 raise N2VCBadArgumentsException(
795 message
="namespace is mandatory", bad_args
=["namespace"]
804 ) = self
._get
_namespace
_components
(namespace
=namespace
)
805 if ns_id
is not None:
807 models
= await libjuju
.list_models(contains
=ns_id
)
809 await libjuju
.destroy_model(
810 model_name
=model
, total_timeout
=total_timeout
812 except Exception as e
:
813 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
815 message
="Error deleting namespace {} : {}".format(
820 raise N2VCBadArgumentsException(
821 message
="only ns_id is permitted to delete yet",
822 bad_args
=["namespace"],
824 except Exception as e
:
825 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
828 self
.delete_namespace_locks
.pop(namespace
)
829 self
.log
.info("Namespace {} deleted".format(namespace
))
831 async def delete_execution_environment(
834 db_dict
: dict = None,
835 total_timeout
: float = None,
836 scaling_in
: bool = False,
837 vca_type
: str = None,
841 Delete an execution environment
842 :param str ee_id: id of the execution environment to delete
843 :param dict db_dict: where to write into database when the status changes.
844 It contains a dict with
845 {collection: <str>, filter: {}, path: <str>},
846 e.g. {collection: "nsrs", filter:
847 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
848 :param: total_timeout: Total timeout
849 :param: scaling_in: Boolean to indicate if it is a scaling in operation
850 :param: vca_type: VCA type
851 :param: vca_id: VCA ID
853 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
854 libjuju
= await self
._get
_libjuju
(vca_id
)
858 raise N2VCBadArgumentsException(
859 message
="ee_id is mandatory", bad_args
=["ee_id"]
862 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
868 await libjuju
.destroy_model(
869 model_name
=model_name
, total_timeout
=total_timeout
871 elif vca_type
== "native_charm" and scaling_in
:
872 # destroy the unit in the application
873 await libjuju
.destroy_unit(
874 application_name
=application_name
,
875 model_name
=model_name
,
876 machine_id
=machine_id
,
877 total_timeout
=total_timeout
,
880 # destroy the application
881 await libjuju
.destroy_application(
882 model_name
=model_name
,
883 application_name
=application_name
,
884 total_timeout
=total_timeout
,
886 except Exception as e
:
889 "Error deleting execution environment {} (application {}) : {}"
890 ).format(ee_id
, application_name
, e
)
893 self
.log
.info("Execution environment {} deleted".format(ee_id
))
895 async def exec_primitive(
900 db_dict
: dict = None,
901 progress_timeout
: float = None,
902 total_timeout
: float = None,
904 vca_type
: str = None,
907 Execute a primitive in the execution environment
909 :param: ee_id: the one returned by create_execution_environment or
910 register_execution_environment
911 :param: primitive_name: must be one defined in the software. There is one
912 called 'config', where, for the proxy case, the 'credentials' of VM are
914 :param: params_dict: parameters of the action
915 :param: db_dict: where to write into database when the status changes.
916 It contains a dict with
917 {collection: <str>, filter: {}, path: <str>},
918 e.g. {collection: "nsrs", filter:
919 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
920 :param: progress_timeout: Progress timeout
921 :param: total_timeout: Total timeout
922 :param: vca_id: VCA ID
923 :param: vca_type: VCA type
924 :returns str: primitive result, if ok. It raises exceptions in case of fail
928 "Executing primitive: {} on ee: {}, params: {}".format(
929 primitive_name
, ee_id
, params_dict
932 libjuju
= await self
._get
_libjuju
(vca_id
)
935 if ee_id
is None or len(ee_id
) == 0:
936 raise N2VCBadArgumentsException(
937 message
="ee_id is mandatory", bad_args
=["ee_id"]
939 if primitive_name
is None or len(primitive_name
) == 0:
940 raise N2VCBadArgumentsException(
941 message
="action_name is mandatory", bad_args
=["action_name"]
943 if params_dict
is None:
951 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
952 # To run action on the leader unit in libjuju.execute_action function,
953 # machine_id must be set to None if vca_type is not native_charm
954 if vca_type
!= "native_charm":
957 raise N2VCBadArgumentsException(
958 message
="ee_id={} is not a valid execution environment id".format(
964 if primitive_name
== "config":
965 # Special case: config primitive
967 await libjuju
.configure_application(
968 model_name
=model_name
,
969 application_name
=application_name
,
972 actions
= await libjuju
.get_actions(
973 application_name
=application_name
, model_name
=model_name
976 "Application {} has these actions: {}".format(
977 application_name
, actions
980 if "verify-ssh-credentials" in actions
:
981 # execute verify-credentials
984 for _
in range(num_retries
):
986 self
.log
.debug("Executing action verify-ssh-credentials...")
987 output
, ok
= await libjuju
.execute_action(
988 model_name
=model_name
,
989 application_name
=application_name
,
990 action_name
="verify-ssh-credentials",
992 progress_timeout
=progress_timeout
,
993 total_timeout
=total_timeout
,
998 "Error executing verify-ssh-credentials: {}. Retrying..."
1000 await asyncio
.sleep(retry_timeout
)
1003 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1005 except asyncio
.CancelledError
:
1009 "Error executing verify-ssh-credentials after {} retries. ".format(
1014 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
1017 self
.log
.debug(msg
=msg
)
1018 except Exception as e
:
1019 self
.log
.error("Error configuring juju application: {}".format(e
))
1020 raise N2VCExecutionException(
1021 message
="Error configuring application into ee={} : {}".format(
1024 primitive_name
=primitive_name
,
1029 output
, status
= await libjuju
.execute_action(
1030 model_name
=model_name
,
1031 application_name
=application_name
,
1032 action_name
=primitive_name
,
1034 machine_id
=machine_id
,
1035 progress_timeout
=progress_timeout
,
1036 total_timeout
=total_timeout
,
1039 if status
== "completed":
1042 if "output" in output
:
1043 raise Exception(f
'{status}: {output["output"]}')
1046 f
"{status}: No further information received from action"
1049 except Exception as e
:
1050 self
.log
.error(f
"Error executing primitive {primitive_name}: {e}")
1051 raise N2VCExecutionException(
1052 message
=f
"Error executing primitive {primitive_name} in ee={ee_id}: {e}",
1053 primitive_name
=primitive_name
,
1056 async def upgrade_charm(
1060 charm_id
: str = None,
1061 charm_type
: str = None,
1062 timeout
: float = None,
1064 """This method upgrade charms in VNFs
1067 ee_id: Execution environment id
1068 path: Local path to the charm
1070 charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm
1071 timeout: (Float) Timeout for the ns update operation
1074 The output of the update operation if status equals to "completed"
1077 self
.log
.info("Upgrading charm: {} on ee: {}".format(path
, ee_id
))
1078 libjuju
= await self
._get
_libjuju
(charm_id
)
1081 if ee_id
is None or len(ee_id
) == 0:
1082 raise N2VCBadArgumentsException(
1083 message
="ee_id is mandatory", bad_args
=["ee_id"]
1090 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
1093 raise N2VCBadArgumentsException(
1094 message
="ee_id={} is not a valid execution environment id".format(
1101 await libjuju
.upgrade_charm(
1102 application_name
=application_name
,
1104 model_name
=model_name
,
1105 total_timeout
=timeout
,
1108 return f
"Charm upgraded with application name {application_name}"
1110 except Exception as e
:
1111 self
.log
.error("Error upgrading charm {}: {}".format(path
, e
))
1113 raise N2VCException(
1114 message
="Error upgrading charm {} in ee={} : {}".format(path
, ee_id
, e
)
1117 async def disconnect(self
, vca_id
: str = None):
1121 :param: vca_id: VCA ID
1123 self
.log
.info("closing juju N2VC...")
1124 libjuju
= await self
._get
_libjuju
(vca_id
)
1126 await libjuju
.disconnect()
1127 except Exception as e
:
1128 raise N2VCConnectionException(
1129 message
="Error disconnecting controller: {}".format(e
),
1130 url
=libjuju
.vca_connection
.data
.endpoints
,
1134 ####################################################################################
1135 ################################### P R I V A T E ##################################
1136 ####################################################################################
1139 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1143 :param: vca_id: VCA ID
1144 If None, get a libjuju object with a Connection to the default VCA
1145 Else, geta libjuju object with a Connection to the specified VCA
1148 while self
.loading_libjuju
.locked():
1149 await asyncio
.sleep(0.1)
1150 if not self
.libjuju
:
1151 async with self
.loading_libjuju
:
1152 vca_connection
= await get_connection(self
._store
)
1153 self
.libjuju
= Libjuju(vca_connection
, log
=self
.log
)
1156 vca_connection
= await get_connection(self
._store
, vca_id
)
1157 return Libjuju(vca_connection
, log
=self
.log
, n2vc
=self
)
1159 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
, log
=self
.log
, n2vc
=self
)
1542 controller
= await libjuju
.get_controller()
1543 await libjuju
.disconnect_controller(controller
)