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
.exceptions
import (
28 N2VCBadArgumentsException
,
30 N2VCConnectionException
,
31 N2VCExecutionException
,
35 from n2vc
.n2vc_conn
import N2VCConnector
36 from n2vc
.n2vc_conn
import obj_to_dict
, obj_to_yaml
37 from n2vc
.libjuju
import Libjuju
38 from n2vc
.store
import MotorStore
39 from n2vc
.vca
.connection
import get_connection
42 class N2VCJujuConnector(N2VCConnector
):
45 ####################################################################################
46 ################################### P U B L I C ####################################
47 ####################################################################################
50 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
64 :param: db: Database object from osm_common
65 :param: fs: Filesystem object from osm_common
67 :param: loop: Asyncio loop
68 :param: on_update_db: Callback function to be called for updating the database.
71 # parent class constructor
72 N2VCConnector
.__init
__(
78 on_update_db
=on_update_db
,
81 # silence websocket traffic log
82 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
83 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
84 logging
.getLogger("model").setLevel(logging
.WARN
)
86 self
.log
.info("Initializing N2VC juju connector...")
88 db_uri
= EnvironConfig(prefixes
=["OSMLCM_", "OSMMON_"]).get("database_uri")
89 self
._store
= MotorStore(db_uri
)
90 self
.loading_libjuju
= asyncio
.Lock(loop
=self
.loop
)
92 self
.log
.info("N2VC juju connector initialized")
95 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
98 Get status from all juju models from a VCA
100 :param namespace: we obtain ns from namespace
101 :param yaml_format: returns a yaml string
102 :param: vca_id: VCA ID from which the status will be retrieved.
104 # TODO: Review where is this function used. It is not optimal at all to get the status
105 # from all the juju models of a particular VCA. Additionally, these models might
106 # not have been deployed by OSM, in that case we are getting information from
107 # deployments outside of OSM's scope.
109 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
110 libjuju
= await self
._get
_libjuju
(vca_id
)
112 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
115 # model name is ns_id
117 if model_name
is None:
118 msg
= "Namespace {} not valid".format(namespace
)
120 raise N2VCBadArgumentsException(msg
, ["namespace"])
123 models
= await libjuju
.list_models(contains
=ns_id
)
126 status
[m
] = await libjuju
.get_model_status(m
)
129 return obj_to_yaml(status
)
131 return obj_to_dict(status
)
133 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
135 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
137 :param vcastatus: dict containing vcaStatus
138 :param: vca_id: VCA ID
143 libjuju
= await self
._get
_libjuju
(vca_id
)
144 for model_name
in vcastatus
:
145 # Adding executed actions
146 vcastatus
[model_name
][
148 ] = await libjuju
.get_executed_actions(model_name
)
149 for application
in vcastatus
[model_name
]["applications"]:
150 # Adding application actions
151 vcastatus
[model_name
]["applications"][application
][
153 ] = await libjuju
.get_actions(application
, model_name
)
154 # Adding application configs
155 vcastatus
[model_name
]["applications"][application
][
157 ] = await libjuju
.get_application_configs(model_name
, application
)
158 except Exception as e
:
159 self
.log
.debug("Error in updating vca status: {}".format(str(e
)))
161 async def create_execution_environment(
165 reuse_ee_id
: str = None,
166 progress_timeout
: float = None,
167 total_timeout
: float = None,
171 Create an Execution Environment. Returns when it is created or raises an
174 :param: namespace: Contains a dot separate string.
175 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
176 :param: db_dict: where to write to database when the status changes.
177 It contains a dictionary with {collection: str, filter: {}, path: str},
178 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
179 "_admin.deployed.VCA.3"}
180 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
182 :param: progress_timeout: Progress timeout
183 :param: total_timeout: Total timeout
184 :param: vca_id: VCA ID
186 :returns: id of the new execution environment and credentials for it
187 (credentials can contains hostname, username, etc depending on underlying cloud)
191 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
192 namespace
, reuse_ee_id
195 libjuju
= await self
._get
_libjuju
(vca_id
)
199 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
209 ) = self
._get
_namespace
_components
(namespace
=namespace
)
210 # model name is ns_id
213 application_name
= self
._get
_application
_name
(namespace
=namespace
)
216 "model name: {}, application name: {}, machine_id: {}".format(
217 model_name
, application_name
, machine_id
221 # create or reuse a new juju machine
223 if not await libjuju
.model_exists(model_name
):
224 await libjuju
.add_model(
226 libjuju
.vca_connection
.lxd_cloud
,
228 machine
, new
= await libjuju
.create_machine(
229 model_name
=model_name
,
230 machine_id
=machine_id
,
232 progress_timeout
=progress_timeout
,
233 total_timeout
=total_timeout
,
235 # id for the execution environment
236 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
237 model_name
=model_name
,
238 application_name
=application_name
,
239 machine_id
=str(machine
.entity_id
),
241 self
.log
.debug("ee_id: {}".format(ee_id
))
244 # write ee_id in database
245 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
247 except Exception as e
:
248 message
= "Error creating machine on juju: {}".format(e
)
249 self
.log
.error(message
)
250 raise N2VCException(message
=message
)
252 # new machine credentials
254 "hostname": machine
.dns_name
,
258 "Execution environment created. ee_id: {}, credentials: {}".format(
263 return ee_id
, credentials
265 async def register_execution_environment(
270 progress_timeout
: float = None,
271 total_timeout
: float = None,
275 Register an existing execution environment at the VCA
277 :param: namespace: Contains a dot separate string.
278 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
279 :param: credentials: credentials to access the existing execution environment
280 (it can contains hostname, username, path to private key,
281 etc depending on underlying cloud)
282 :param: db_dict: where to write to database when the status changes.
283 It contains a dictionary with {collection: str, filter: {}, path: str},
284 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
285 "_admin.deployed.VCA.3"}
286 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
288 :param: progress_timeout: Progress timeout
289 :param: total_timeout: Total timeout
290 :param: vca_id: VCA ID
292 :returns: id of the execution environment
295 "Registering execution environment. namespace={}, credentials={}".format(
296 namespace
, credentials
299 libjuju
= await self
._get
_libjuju
(vca_id
)
301 if credentials
is None:
302 raise N2VCBadArgumentsException(
303 message
="credentials are mandatory", bad_args
=["credentials"]
305 if credentials
.get("hostname"):
306 hostname
= credentials
["hostname"]
308 raise N2VCBadArgumentsException(
309 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
311 if credentials
.get("username"):
312 username
= credentials
["username"]
314 raise N2VCBadArgumentsException(
315 message
="username is mandatory", bad_args
=["credentials.username"]
317 if "private_key_path" in credentials
:
318 private_key_path
= credentials
["private_key_path"]
320 # if not passed as argument, use generated private key path
321 private_key_path
= self
.private_key_path
323 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
330 application_name
= self
._get
_application
_name
(namespace
=namespace
)
332 # register machine on juju
334 if not await libjuju
.model_exists(model_name
):
335 await libjuju
.add_model(
337 libjuju
.vca_connection
.lxd_cloud
,
339 machine_id
= await libjuju
.provision_machine(
340 model_name
=model_name
,
343 private_key_path
=private_key_path
,
345 progress_timeout
=progress_timeout
,
346 total_timeout
=total_timeout
,
348 except Exception as e
:
349 self
.log
.error("Error registering machine: {}".format(e
))
351 message
="Error registering machine on juju: {}".format(e
)
354 self
.log
.info("Machine registered: {}".format(machine_id
))
356 # id for the execution environment
357 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
358 model_name
=model_name
,
359 application_name
=application_name
,
360 machine_id
=str(machine_id
),
363 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
367 async def install_configuration_sw(
372 progress_timeout
: float = None,
373 total_timeout
: float = 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
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
, mode
="dir"):
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 await libjuju
.deploy_charm(
457 model_name
=model_name
,
458 application_name
=application_name
,
460 machine_id
=machine_id
,
462 progress_timeout
=progress_timeout
,
463 total_timeout
=total_timeout
,
467 except Exception as e
:
469 message
="Error desploying charm into ee={} : {}".format(ee_id
, e
)
472 self
.log
.info("Configuration sw installed")
474 async def install_k8s_proxy_charm(
480 progress_timeout
: float = None,
481 total_timeout
: float = None,
486 Install a k8s proxy charm
488 :param charm_name: Name of the charm being deployed
489 :param namespace: collection of all the uuids related to the charm.
490 :param str artifact_path: where to locate the artifacts (parent folder) using
492 the final artifact path will be a combination of this artifact_path and
493 additional string from the config_dict (e.g. charm name)
494 :param dict db_dict: where to write into database when the status changes.
495 It contains a dict with
496 {collection: <str>, filter: {}, path: <str>},
497 e.g. {collection: "nsrs", filter:
498 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
499 :param: progress_timeout: Progress timeout
500 :param: total_timeout: Total timeout
501 :param config: Dictionary with additional configuration
502 :param vca_id: VCA ID
504 :returns ee_id: execution environment id.
507 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
508 charm_name
, artifact_path
, db_dict
511 libjuju
= await self
._get
_libjuju
(vca_id
)
513 if artifact_path
is None or len(artifact_path
) == 0:
514 raise N2VCBadArgumentsException(
515 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
518 raise N2VCBadArgumentsException(
519 message
="db_dict is mandatory", bad_args
=["db_dict"]
522 # remove // in charm path
523 while artifact_path
.find("//") >= 0:
524 artifact_path
= artifact_path
.replace("//", "/")
527 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
528 msg
= "artifact path does not exist: {}".format(artifact_path
)
529 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
531 if artifact_path
.startswith("/"):
532 full_path
= self
.fs
.path
+ artifact_path
534 full_path
= self
.fs
.path
+ "/" + artifact_path
536 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
537 model_name
= "{}-k8s".format(ns_id
)
538 if not await libjuju
.model_exists(model_name
):
539 await libjuju
.add_model(
541 libjuju
.vca_connection
.k8s_cloud
,
543 application_name
= self
._get
_application
_name
(namespace
)
546 await libjuju
.deploy_charm(
547 model_name
=model_name
,
548 application_name
=application_name
,
552 progress_timeout
=progress_timeout
,
553 total_timeout
=total_timeout
,
556 except Exception as e
:
557 raise N2VCException(message
="Error deploying charm: {}".format(e
))
559 self
.log
.info("K8s proxy charm installed")
560 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
561 model_name
=model_name
,
562 application_name
=application_name
,
566 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
570 async def get_ee_ssh_public__key(
574 progress_timeout
: float = None,
575 total_timeout
: float = None,
579 Get Execution environment ssh public key
581 :param: ee_id: the id of the execution environment returned by
582 create_execution_environment or register_execution_environment
583 :param: db_dict: where to write into database when the status changes.
584 It contains a dict with
585 {collection: <str>, filter: {}, path: <str>},
586 e.g. {collection: "nsrs", filter:
587 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
588 :param: progress_timeout: Progress timeout
589 :param: total_timeout: Total timeout
590 :param vca_id: VCA ID
591 :returns: public key of the execution environment
592 For the case of juju proxy charm ssh-layered, it is the one
593 returned by 'get-ssh-public-key' primitive.
594 It raises a N2VC exception if fails
599 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
600 ).format(ee_id
, db_dict
)
602 libjuju
= await self
._get
_libjuju
(vca_id
)
605 if ee_id
is None or len(ee_id
) == 0:
606 raise N2VCBadArgumentsException(
607 message
="ee_id is mandatory", bad_args
=["ee_id"]
610 raise N2VCBadArgumentsException(
611 message
="db_dict is mandatory", bad_args
=["db_dict"]
619 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
621 "model: {}, application: {}, machine: {}".format(
622 model_name
, application_name
, machine_id
626 raise N2VCBadArgumentsException(
627 message
="ee_id={} is not a valid execution environment id".format(
633 # try to execute ssh layer primitives (if exist):
639 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
641 # execute action: generate-ssh-key
643 output
, _status
= await libjuju
.execute_action(
644 model_name
=model_name
,
645 application_name
=application_name
,
646 action_name
="generate-ssh-key",
648 progress_timeout
=progress_timeout
,
649 total_timeout
=total_timeout
,
651 except Exception as e
:
653 "Skipping exception while executing action generate-ssh-key: {}".format(
658 # execute action: get-ssh-public-key
660 output
, _status
= await libjuju
.execute_action(
661 model_name
=model_name
,
662 application_name
=application_name
,
663 action_name
="get-ssh-public-key",
665 progress_timeout
=progress_timeout
,
666 total_timeout
=total_timeout
,
668 except Exception as e
:
669 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
671 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
673 # return public key if exists
674 return output
["pubkey"] if "pubkey" in output
else output
676 async def get_metrics(
677 self
, model_name
: str, application_name
: str, vca_id
: str = None
680 Get metrics from application
682 :param: model_name: Model name
683 :param: application_name: Application name
684 :param: vca_id: VCA ID
686 :return: Dictionary with obtained metrics
688 libjuju
= await self
._get
_libjuju
(vca_id
)
689 return await libjuju
.get_metrics(model_name
, application_name
)
691 async def add_relation(
700 Add relation between two charmed endpoints
702 :param: ee_id_1: The id of the first execution environment
703 :param: ee_id_2: The id of the second execution environment
704 :param: endpoint_1: The endpoint in the first execution environment
705 :param: endpoint_2: The endpoint in the second execution environment
706 :param: vca_id: VCA ID
709 "adding new relation between {} and {}, endpoints: {}, {}".format(
710 ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
713 libjuju
= await self
._get
_libjuju
(vca_id
)
717 message
= "EE 1 is mandatory"
718 self
.log
.error(message
)
719 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_1"])
721 message
= "EE 2 is mandatory"
722 self
.log
.error(message
)
723 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_2"])
725 message
= "endpoint 1 is mandatory"
726 self
.log
.error(message
)
727 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_1"])
729 message
= "endpoint 2 is mandatory"
730 self
.log
.error(message
)
731 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_2"])
733 # get the model, the applications and the machines from the ee_id's
734 model_1
, app_1
, _machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
735 model_2
, app_2
, _machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
737 # model must be the same
738 if model_1
!= model_2
:
739 message
= "EE models are not the same: {} vs {}".format(ee_id_1
, ee_id_2
)
740 self
.log
.error(message
)
741 raise N2VCBadArgumentsException(
742 message
=message
, bad_args
=["ee_id_1", "ee_id_2"]
745 # add juju relations between two applications
747 await libjuju
.add_relation(
749 endpoint_1
="{}:{}".format(app_1
, endpoint_1
),
750 endpoint_2
="{}:{}".format(app_2
, endpoint_2
),
752 except Exception as e
:
753 message
= "Error adding relation between {} and {}: {}".format(
756 self
.log
.error(message
)
757 raise N2VCException(message
=message
)
759 async def remove_relation(self
):
761 self
.log
.info("Method not implemented yet")
762 raise MethodNotImplemented()
764 async def deregister_execution_environments(self
):
765 self
.log
.info("Method not implemented yet")
766 raise MethodNotImplemented()
768 async def delete_namespace(
771 db_dict
: dict = None,
772 total_timeout
: float = None,
776 Remove a network scenario and its execution environments
777 :param: namespace: [<nsi-id>].<ns-id>
778 :param: db_dict: where to write into database when the status changes.
779 It contains a dict with
780 {collection: <str>, filter: {}, path: <str>},
781 e.g. {collection: "nsrs", filter:
782 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
783 :param: total_timeout: Total timeout
784 :param: vca_id: VCA ID
786 self
.log
.info("Deleting namespace={}".format(namespace
))
787 libjuju
= await self
._get
_libjuju
(vca_id
)
790 if namespace
is None:
791 raise N2VCBadArgumentsException(
792 message
="namespace is mandatory", bad_args
=["namespace"]
795 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
798 if ns_id
is not None:
800 models
= await libjuju
.list_models(contains
=ns_id
)
802 await libjuju
.destroy_model(
803 model_name
=model
, total_timeout
=total_timeout
805 except Exception as e
:
807 message
="Error deleting namespace {} : {}".format(namespace
, e
)
810 raise N2VCBadArgumentsException(
811 message
="only ns_id is permitted to delete yet", bad_args
=["namespace"]
814 self
.log
.info("Namespace {} deleted".format(namespace
))
816 async def delete_execution_environment(
819 db_dict
: dict = None,
820 total_timeout
: float = None,
821 scaling_in
: bool = False,
825 Delete an execution environment
826 :param str ee_id: id of the execution environment to delete
827 :param dict db_dict: where to write into database when the status changes.
828 It contains a dict with
829 {collection: <str>, filter: {}, path: <str>},
830 e.g. {collection: "nsrs", filter:
831 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
832 :param: total_timeout: Total timeout
833 :param: scaling_in: Boolean to indicate if is it a scaling in operation
834 :param: vca_id: VCA ID
836 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
837 libjuju
= await self
._get
_libjuju
(vca_id
)
841 raise N2VCBadArgumentsException(
842 message
="ee_id is mandatory", bad_args
=["ee_id"]
845 model_name
, application_name
, _machine_id
= self
._get
_ee
_id
_components
(
851 # TODO: should this be removed?
852 await libjuju
.destroy_model(
853 model_name
=model_name
,
854 total_timeout
=total_timeout
,
857 # destroy the application
858 await libjuju
.destroy_application(
859 model_name
=model_name
,
860 application_name
=application_name
,
861 total_timeout
=total_timeout
,
863 except Exception as e
:
866 "Error deleting execution environment {} (application {}) : {}"
867 ).format(ee_id
, application_name
, e
)
870 self
.log
.info("Execution environment {} deleted".format(ee_id
))
872 async def exec_primitive(
877 db_dict
: dict = None,
878 progress_timeout
: float = None,
879 total_timeout
: float = None,
883 Execute a primitive in the execution environment
885 :param: ee_id: the one returned by create_execution_environment or
886 register_execution_environment
887 :param: primitive_name: must be one defined in the software. There is one
888 called 'config', where, for the proxy case, the 'credentials' of VM are
890 :param: params_dict: parameters of the action
891 :param: db_dict: where to write into database when the status changes.
892 It contains a dict with
893 {collection: <str>, filter: {}, path: <str>},
894 e.g. {collection: "nsrs", filter:
895 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
896 :param: progress_timeout: Progress timeout
897 :param: total_timeout: Total timeout
898 :param: vca_id: VCA ID
899 :returns str: primitive result, if ok. It raises exceptions in case of fail
903 "Executing primitive: {} on ee: {}, params: {}".format(
904 primitive_name
, ee_id
, params_dict
907 libjuju
= await self
._get
_libjuju
(vca_id
)
910 if ee_id
is None or len(ee_id
) == 0:
911 raise N2VCBadArgumentsException(
912 message
="ee_id is mandatory", bad_args
=["ee_id"]
914 if primitive_name
is None or len(primitive_name
) == 0:
915 raise N2VCBadArgumentsException(
916 message
="action_name is mandatory", bad_args
=["action_name"]
918 if params_dict
is None:
926 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
928 raise N2VCBadArgumentsException(
929 message
="ee_id={} is not a valid execution environment id".format(
935 if primitive_name
== "config":
936 # Special case: config primitive
938 await libjuju
.configure_application(
939 model_name
=model_name
,
940 application_name
=application_name
,
943 actions
= await libjuju
.get_actions(
944 application_name
=application_name
,
945 model_name
=model_name
,
948 "Application {} has these actions: {}".format(
949 application_name
, actions
952 if "verify-ssh-credentials" in actions
:
953 # execute verify-credentials
956 for _
in range(num_retries
):
958 self
.log
.debug("Executing action verify-ssh-credentials...")
959 output
, ok
= await libjuju
.execute_action(
960 model_name
=model_name
,
961 application_name
=application_name
,
962 action_name
="verify-ssh-credentials",
964 progress_timeout
=progress_timeout
,
965 total_timeout
=total_timeout
,
970 "Error executing verify-ssh-credentials: {}. Retrying..."
972 await asyncio
.sleep(retry_timeout
)
975 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
977 except asyncio
.CancelledError
:
981 "Error executing verify-ssh-credentials after {} retries. ".format(
986 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
989 self
.log
.debug(msg
=msg
)
990 except Exception as e
:
991 self
.log
.error("Error configuring juju application: {}".format(e
))
992 raise N2VCExecutionException(
993 message
="Error configuring application into ee={} : {}".format(
996 primitive_name
=primitive_name
,
1001 output
, status
= await libjuju
.execute_action(
1002 model_name
=model_name
,
1003 application_name
=application_name
,
1004 action_name
=primitive_name
,
1006 progress_timeout
=progress_timeout
,
1007 total_timeout
=total_timeout
,
1010 if status
== "completed":
1013 raise Exception("status is not completed: {}".format(status
))
1014 except Exception as e
:
1016 "Error executing primitive {}: {}".format(primitive_name
, e
)
1018 raise N2VCExecutionException(
1019 message
="Error executing primitive {} into ee={} : {}".format(
1020 primitive_name
, ee_id
, e
1022 primitive_name
=primitive_name
,
1025 async def disconnect(self
, vca_id
: str = None):
1029 :param: vca_id: VCA ID
1031 self
.log
.info("closing juju N2VC...")
1032 libjuju
= await self
._get
_libjuju
(vca_id
)
1034 await libjuju
.disconnect()
1035 except Exception as e
:
1036 raise N2VCConnectionException(
1037 message
="Error disconnecting controller: {}".format(e
),
1038 url
=libjuju
.vca_connection
.data
.endpoints
,
1042 ####################################################################################
1043 ################################### P R I V A T E ##################################
1044 ####################################################################################
1047 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1051 :param: vca_id: VCA ID
1052 If None, get a libjuju object with a Connection to the default VCA
1053 Else, geta libjuju object with a Connection to the specified VCA
1056 while self
.loading_libjuju
.locked():
1057 await asyncio
.sleep(0.1)
1058 if not self
.libjuju
:
1059 async with self
.loading_libjuju
:
1060 vca_connection
= await get_connection(self
._store
)
1061 self
.libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
)
1064 vca_connection
= await get_connection(self
._store
, vca_id
)
1072 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
1074 # write ee_id to database: _admin.deployed.VCA.x
1076 the_table
= db_dict
["collection"]
1077 the_filter
= db_dict
["filter"]
1078 the_path
= db_dict
["path"]
1079 if not the_path
[-1] == ".":
1080 the_path
= the_path
+ "."
1081 update_dict
= {the_path
+ "ee_id": ee_id
}
1082 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1085 q_filter
=the_filter
,
1086 update_dict
=update_dict
,
1089 except asyncio
.CancelledError
:
1091 except Exception as e
:
1092 self
.log
.error("Error writing ee_id to database: {}".format(e
))
1095 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
1097 Build an execution environment id form model, application and machine
1099 :param application_name:
1103 # id for the execution environment
1104 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1107 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1109 Get model, application and machine components from an execution environment id
1111 :return: model_name, application_name, machine_id
1115 return None, None, None
1117 # split components of id
1118 parts
= ee_id
.split(".")
1119 model_name
= parts
[0]
1120 application_name
= parts
[1]
1121 machine_id
= parts
[2]
1122 return model_name
, application_name
, machine_id
1124 def _get_application_name(self
, namespace
: str) -> str:
1126 Build application name from namespace
1128 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1131 # TODO: Enforce the Juju 50-character application limit
1133 # split namespace components
1134 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
1138 if vnf_id
is None or len(vnf_id
) == 0:
1141 # Shorten the vnf_id to its last twelve characters
1142 vnf_id
= "vnf-" + vnf_id
[-12:]
1144 if vdu_id
is None or len(vdu_id
) == 0:
1147 # Shorten the vdu_id to its last twelve characters
1148 vdu_id
= "-vdu-" + vdu_id
[-12:]
1150 if vdu_count
is None or len(vdu_count
) == 0:
1153 vdu_count
= "-cnt-" + vdu_count
1155 application_name
= "app-{}{}{}".format(vnf_id
, vdu_id
, vdu_count
)
1157 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1160 def _format_model_name(name
: str) -> str:
1161 """Format the name of the model.
1163 Model names may only contain lowercase letters, digits and hyphens
1166 return name
.replace("_", "-").replace(" ", "-").lower()
1169 def _format_app_name(name
: str) -> str:
1170 """Format the name of the application (in order to assure valid application name).
1172 Application names have restrictions (run juju deploy --help):
1173 - contains lowercase letters 'a'-'z'
1174 - contains numbers '0'-'9'
1175 - contains hyphens '-'
1176 - starts with a lowercase letter
1177 - not two or more consecutive hyphens
1178 - after a hyphen, not a group with all numbers
1181 def all_numbers(s
: str) -> bool:
1187 new_name
= name
.replace("_", "-")
1188 new_name
= new_name
.replace(" ", "-")
1189 new_name
= new_name
.lower()
1190 while new_name
.find("--") >= 0:
1191 new_name
= new_name
.replace("--", "-")
1192 groups
= new_name
.split("-")
1194 # find 'all numbers' groups and prefix them with a letter
1196 for i
in range(len(groups
)):
1198 if all_numbers(group
):
1204 if app_name
[0].isdigit():
1205 app_name
= "z" + app_name
1209 async def validate_vca(self
, vca_id
: str):
1211 Validate a VCA by connecting/disconnecting to/from it
1213 :param: vca_id: VCA ID
1215 vca_connection
= await get_connection(self
._store
, vca_id
=vca_id
)
1216 libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1217 controller
= await libjuju
.get_controller()
1218 await libjuju
.disconnect_controller(controller
)