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
47 class N2VCJujuConnector(N2VCConnector
):
50 ####################################################################################
51 ################################### P U B L I C ####################################
52 ####################################################################################
55 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
69 :param: db: Database object from osm_common
70 :param: fs: Filesystem object from osm_common
72 :param: loop: Asyncio loop
73 :param: on_update_db: Callback function to be called for updating the database.
76 # parent class constructor
77 N2VCConnector
.__init
__(
83 on_update_db
=on_update_db
,
86 # silence websocket traffic log
87 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
88 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
89 logging
.getLogger("model").setLevel(logging
.WARN
)
91 self
.log
.info("Initializing N2VC juju connector...")
93 db_uri
= EnvironConfig(prefixes
=["OSMLCM_", "OSMMON_"]).get("database_uri")
94 self
._store
= MotorStore(db_uri
)
95 self
.loading_libjuju
= asyncio
.Lock(loop
=self
.loop
)
96 self
.delete_namespace_locks
= {}
97 self
.log
.info("N2VC juju connector initialized")
100 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
103 Get status from all juju models from a VCA
105 :param namespace: we obtain ns from namespace
106 :param yaml_format: returns a yaml string
107 :param: vca_id: VCA ID from which the status will be retrieved.
109 # TODO: Review where is this function used. It is not optimal at all to get the status
110 # from all the juju models of a particular VCA. Additionally, these models might
111 # not have been deployed by OSM, in that case we are getting information from
112 # deployments outside of OSM's scope.
114 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
115 libjuju
= await self
._get
_libjuju
(vca_id
)
117 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
120 # model name is ns_id
122 if model_name
is None:
123 msg
= "Namespace {} not valid".format(namespace
)
125 raise N2VCBadArgumentsException(msg
, ["namespace"])
128 models
= await libjuju
.list_models(contains
=ns_id
)
131 status
[m
] = await libjuju
.get_model_status(m
)
134 return obj_to_yaml(status
)
136 return obj_to_dict(status
)
138 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
140 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
142 :param vcastatus: dict containing vcaStatus
143 :param: vca_id: VCA ID
148 libjuju
= await self
._get
_libjuju
(vca_id
)
149 for model_name
in vcastatus
:
150 # Adding executed actions
151 vcastatus
[model_name
][
153 ] = await libjuju
.get_executed_actions(model_name
)
154 for application
in vcastatus
[model_name
]["applications"]:
155 # Adding application actions
156 vcastatus
[model_name
]["applications"][application
][
158 ] = await libjuju
.get_actions(application
, model_name
)
159 # Adding application configs
160 vcastatus
[model_name
]["applications"][application
][
162 ] = await libjuju
.get_application_configs(model_name
, application
)
163 except Exception as e
:
164 self
.log
.debug("Error in updating vca status: {}".format(str(e
)))
166 async def create_execution_environment(
170 reuse_ee_id
: str = None,
171 progress_timeout
: float = None,
172 total_timeout
: float = None,
176 Create an Execution Environment. Returns when it is created or raises an
179 :param: namespace: Contains a dot separate string.
180 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
181 :param: db_dict: where to write to database when the status changes.
182 It contains a dictionary with {collection: str, filter: {}, path: str},
183 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
184 "_admin.deployed.VCA.3"}
185 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
187 :param: progress_timeout: Progress timeout
188 :param: total_timeout: Total timeout
189 :param: vca_id: VCA ID
191 :returns: id of the new execution environment and credentials for it
192 (credentials can contains hostname, username, etc depending on underlying cloud)
196 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
197 namespace
, reuse_ee_id
200 libjuju
= await self
._get
_libjuju
(vca_id
)
204 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
214 ) = self
._get
_namespace
_components
(namespace
=namespace
)
215 # model name is ns_id
218 application_name
= self
._get
_application
_name
(namespace
=namespace
)
221 "model name: {}, application name: {}, machine_id: {}".format(
222 model_name
, application_name
, machine_id
226 # create or reuse a new juju machine
228 if not await libjuju
.model_exists(model_name
):
229 await libjuju
.add_model(
231 libjuju
.vca_connection
.lxd_cloud
,
233 machine
, new
= await libjuju
.create_machine(
234 model_name
=model_name
,
235 machine_id
=machine_id
,
237 progress_timeout
=progress_timeout
,
238 total_timeout
=total_timeout
,
240 # id for the execution environment
241 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
242 model_name
=model_name
,
243 application_name
=application_name
,
244 machine_id
=str(machine
.entity_id
),
246 self
.log
.debug("ee_id: {}".format(ee_id
))
249 # write ee_id in database
250 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
252 except Exception as e
:
253 message
= "Error creating machine on juju: {}".format(e
)
254 self
.log
.error(message
)
255 raise N2VCException(message
=message
)
257 # new machine credentials
259 "hostname": machine
.dns_name
,
263 "Execution environment created. ee_id: {}, credentials: {}".format(
268 return ee_id
, credentials
270 async def register_execution_environment(
275 progress_timeout
: float = None,
276 total_timeout
: float = None,
280 Register an existing execution environment at the VCA
282 :param: namespace: Contains a dot separate string.
283 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
284 :param: credentials: credentials to access the existing execution environment
285 (it can contains hostname, username, path to private key,
286 etc depending on underlying cloud)
287 :param: db_dict: where to write to database when the status changes.
288 It contains a dictionary with {collection: str, filter: {}, path: str},
289 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
290 "_admin.deployed.VCA.3"}
291 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
293 :param: progress_timeout: Progress timeout
294 :param: total_timeout: Total timeout
295 :param: vca_id: VCA ID
297 :returns: id of the execution environment
300 "Registering execution environment. namespace={}, credentials={}".format(
301 namespace
, credentials
304 libjuju
= await self
._get
_libjuju
(vca_id
)
306 if credentials
is None:
307 raise N2VCBadArgumentsException(
308 message
="credentials are mandatory", bad_args
=["credentials"]
310 if credentials
.get("hostname"):
311 hostname
= credentials
["hostname"]
313 raise N2VCBadArgumentsException(
314 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
316 if credentials
.get("username"):
317 username
= credentials
["username"]
319 raise N2VCBadArgumentsException(
320 message
="username is mandatory", bad_args
=["credentials.username"]
322 if "private_key_path" in credentials
:
323 private_key_path
= credentials
["private_key_path"]
325 # if not passed as argument, use generated private key path
326 private_key_path
= self
.private_key_path
328 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
335 application_name
= self
._get
_application
_name
(namespace
=namespace
)
337 # register machine on juju
339 if not await libjuju
.model_exists(model_name
):
340 await libjuju
.add_model(
342 libjuju
.vca_connection
.lxd_cloud
,
344 machine_id
= await libjuju
.provision_machine(
345 model_name
=model_name
,
348 private_key_path
=private_key_path
,
350 progress_timeout
=progress_timeout
,
351 total_timeout
=total_timeout
,
353 except Exception as e
:
354 self
.log
.error("Error registering machine: {}".format(e
))
356 message
="Error registering machine on juju: {}".format(e
)
359 self
.log
.info("Machine registered: {}".format(machine_id
))
361 # id for the execution environment
362 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
363 model_name
=model_name
,
364 application_name
=application_name
,
365 machine_id
=str(machine_id
),
368 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
372 # In case of native_charm is being deployed, if JujuApplicationExists error happens
373 # it will try to add_unit
374 @retry(attempts
=3, delay
=5, retry_exceptions
=(N2VCApplicationExists
,), timeout
=None)
375 async def install_configuration_sw(
380 progress_timeout
: float = None,
381 total_timeout
: float = None,
385 scaling_out
: bool = False,
386 vca_type
: str = None,
389 Install the software inside the execution environment identified by ee_id
391 :param: ee_id: the id of the execution environment returned by
392 create_execution_environment or register_execution_environment
393 :param: artifact_path: where to locate the artifacts (parent folder) using
395 the final artifact path will be a combination of this
396 artifact_path and additional string from the config_dict
398 :param: db_dict: where to write into database when the status changes.
399 It contains a dict with
400 {collection: <str>, filter: {}, path: <str>},
401 e.g. {collection: "nsrs", filter:
402 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
403 :param: progress_timeout: Progress timeout
404 :param: total_timeout: Total timeout
405 :param: config: Dictionary with deployment config information.
406 :param: num_units: Number of units to deploy of a particular charm.
407 :param: vca_id: VCA ID
408 :param: scaling_out: Boolean to indicate if it is a scaling out operation
409 :param: vca_type: VCA type
414 "Installing configuration sw on ee_id: {}, "
415 "artifact path: {}, db_dict: {}"
416 ).format(ee_id
, artifact_path
, db_dict
)
418 libjuju
= await self
._get
_libjuju
(vca_id
)
421 if ee_id
is None or len(ee_id
) == 0:
422 raise N2VCBadArgumentsException(
423 message
="ee_id is mandatory", bad_args
=["ee_id"]
425 if artifact_path
is None or len(artifact_path
) == 0:
426 raise N2VCBadArgumentsException(
427 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
430 raise N2VCBadArgumentsException(
431 message
="db_dict is mandatory", bad_args
=["db_dict"]
439 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
441 "model: {}, application: {}, machine: {}".format(
442 model_name
, application_name
, machine_id
446 raise N2VCBadArgumentsException(
447 message
="ee_id={} is not a valid execution environment id".format(
453 # remove // in charm path
454 while artifact_path
.find("//") >= 0:
455 artifact_path
= artifact_path
.replace("//", "/")
458 if not self
.fs
.file_exists(artifact_path
):
459 msg
= "artifact path does not exist: {}".format(artifact_path
)
460 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
462 if artifact_path
.startswith("/"):
463 full_path
= self
.fs
.path
+ artifact_path
465 full_path
= self
.fs
.path
+ "/" + artifact_path
468 if vca_type
== "native_charm" and await libjuju
.check_application_exists(
469 model_name
, application_name
471 await libjuju
.add_unit(
472 application_name
=application_name
,
473 model_name
=model_name
,
474 machine_id
=machine_id
,
476 progress_timeout
=progress_timeout
,
477 total_timeout
=total_timeout
,
480 await libjuju
.deploy_charm(
481 model_name
=model_name
,
482 application_name
=application_name
,
484 machine_id
=machine_id
,
486 progress_timeout
=progress_timeout
,
487 total_timeout
=total_timeout
,
491 except JujuApplicationExists
as e
:
492 raise N2VCApplicationExists(
493 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
.message
)
495 except Exception as e
:
497 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
)
500 self
.log
.info("Configuration sw installed")
502 async def install_k8s_proxy_charm(
508 progress_timeout
: float = None,
509 total_timeout
: float = None,
514 Install a k8s proxy charm
516 :param charm_name: Name of the charm being deployed
517 :param namespace: collection of all the uuids related to the charm.
518 :param str artifact_path: where to locate the artifacts (parent folder) using
520 the final artifact path will be a combination of this artifact_path and
521 additional string from the config_dict (e.g. charm name)
522 :param dict db_dict: where to write into database when the status changes.
523 It contains a dict with
524 {collection: <str>, filter: {}, path: <str>},
525 e.g. {collection: "nsrs", filter:
526 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
527 :param: progress_timeout: Progress timeout
528 :param: total_timeout: Total timeout
529 :param config: Dictionary with additional configuration
530 :param vca_id: VCA ID
532 :returns ee_id: execution environment id.
535 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
536 charm_name
, artifact_path
, db_dict
539 libjuju
= await self
._get
_libjuju
(vca_id
)
541 if artifact_path
is None or len(artifact_path
) == 0:
542 raise N2VCBadArgumentsException(
543 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
546 raise N2VCBadArgumentsException(
547 message
="db_dict is mandatory", bad_args
=["db_dict"]
550 # remove // in charm path
551 while artifact_path
.find("//") >= 0:
552 artifact_path
= artifact_path
.replace("//", "/")
555 if not self
.fs
.file_exists(artifact_path
):
556 msg
= "artifact path does not exist: {}".format(artifact_path
)
557 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
559 if artifact_path
.startswith("/"):
560 full_path
= self
.fs
.path
+ artifact_path
562 full_path
= self
.fs
.path
+ "/" + artifact_path
564 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
565 model_name
= "{}-k8s".format(ns_id
)
566 if not await libjuju
.model_exists(model_name
):
567 await libjuju
.add_model(
569 libjuju
.vca_connection
.k8s_cloud
,
571 application_name
= self
._get
_application
_name
(namespace
)
574 await libjuju
.deploy_charm(
575 model_name
=model_name
,
576 application_name
=application_name
,
580 progress_timeout
=progress_timeout
,
581 total_timeout
=total_timeout
,
584 except Exception as e
:
585 raise N2VCException(message
="Error deploying charm: {}".format(e
))
587 self
.log
.info("K8s proxy charm installed")
588 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
589 model_name
=model_name
,
590 application_name
=application_name
,
594 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
598 async def get_ee_ssh_public__key(
602 progress_timeout
: float = None,
603 total_timeout
: float = None,
607 Get Execution environment ssh public key
609 :param: ee_id: the id of the execution environment returned by
610 create_execution_environment or register_execution_environment
611 :param: db_dict: where to write into database when the status changes.
612 It contains a dict with
613 {collection: <str>, filter: {}, path: <str>},
614 e.g. {collection: "nsrs", filter:
615 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
616 :param: progress_timeout: Progress timeout
617 :param: total_timeout: Total timeout
618 :param vca_id: VCA ID
619 :returns: public key of the execution environment
620 For the case of juju proxy charm ssh-layered, it is the one
621 returned by 'get-ssh-public-key' primitive.
622 It raises a N2VC exception if fails
627 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
628 ).format(ee_id
, db_dict
)
630 libjuju
= await self
._get
_libjuju
(vca_id
)
633 if ee_id
is None or len(ee_id
) == 0:
634 raise N2VCBadArgumentsException(
635 message
="ee_id is mandatory", bad_args
=["ee_id"]
638 raise N2VCBadArgumentsException(
639 message
="db_dict is mandatory", bad_args
=["db_dict"]
647 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
649 "model: {}, application: {}, machine: {}".format(
650 model_name
, application_name
, machine_id
654 raise N2VCBadArgumentsException(
655 message
="ee_id={} is not a valid execution environment id".format(
661 # try to execute ssh layer primitives (if exist):
667 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
669 # execute action: generate-ssh-key
671 output
, _status
= await libjuju
.execute_action(
672 model_name
=model_name
,
673 application_name
=application_name
,
674 action_name
="generate-ssh-key",
676 progress_timeout
=progress_timeout
,
677 total_timeout
=total_timeout
,
679 except Exception as e
:
681 "Skipping exception while executing action generate-ssh-key: {}".format(
686 # execute action: get-ssh-public-key
688 output
, _status
= await libjuju
.execute_action(
689 model_name
=model_name
,
690 application_name
=application_name
,
691 action_name
="get-ssh-public-key",
693 progress_timeout
=progress_timeout
,
694 total_timeout
=total_timeout
,
696 except Exception as e
:
697 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
699 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
701 # return public key if exists
702 return output
["pubkey"] if "pubkey" in output
else output
704 async def get_metrics(
705 self
, model_name
: str, application_name
: str, vca_id
: str = None
708 Get metrics from application
710 :param: model_name: Model name
711 :param: application_name: Application name
712 :param: vca_id: VCA ID
714 :return: Dictionary with obtained metrics
716 libjuju
= await self
._get
_libjuju
(vca_id
)
717 return await libjuju
.get_metrics(model_name
, application_name
)
719 async def add_relation(
721 provider
: RelationEndpoint
,
722 requirer
: RelationEndpoint
,
725 Add relation between two charmed endpoints
727 :param: provider: Provider relation endpoint
728 :param: requirer: Requirer relation endpoint
730 self
.log
.debug(f
"adding new relation between {provider} and {requirer}")
731 cross_model_relation
= (
732 provider
.model_name
!= requirer
.model_name
733 or requirer
.vca_id
!= requirer
.vca_id
736 if cross_model_relation
:
737 # Cross-model relation
738 provider_libjuju
= await self
._get
_libjuju
(provider
.vca_id
)
739 requirer_libjuju
= await self
._get
_libjuju
(requirer
.vca_id
)
740 offer
= await provider_libjuju
.offer(provider
)
742 saas_name
= await requirer_libjuju
.consume(
743 requirer
.model_name
, offer
, provider_libjuju
745 await requirer_libjuju
.add_relation(
752 vca_id
= provider
.vca_id
753 model
= provider
.model_name
754 libjuju
= await self
._get
_libjuju
(vca_id
)
755 # add juju relations between two applications
756 await libjuju
.add_relation(
758 endpoint_1
=provider
.endpoint
,
759 endpoint_2
=requirer
.endpoint
,
761 except Exception as e
:
762 message
= f
"Error adding relation between {provider} and {requirer}: {e}"
763 self
.log
.error(message
)
764 raise N2VCException(message
=message
)
766 async def remove_relation(self
):
768 self
.log
.info("Method not implemented yet")
769 raise MethodNotImplemented()
771 async def deregister_execution_environments(self
):
772 self
.log
.info("Method not implemented yet")
773 raise MethodNotImplemented()
775 async def delete_namespace(
778 db_dict
: dict = None,
779 total_timeout
: float = None,
783 Remove a network scenario and its execution environments
784 :param: namespace: [<nsi-id>].<ns-id>
785 :param: db_dict: where to write into database when the status changes.
786 It contains a dict with
787 {collection: <str>, filter: {}, path: <str>},
788 e.g. {collection: "nsrs", filter:
789 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
790 :param: total_timeout: Total timeout
791 :param: vca_id: VCA ID
793 self
.log
.info("Deleting namespace={}".format(namespace
))
794 will_not_delete
= False
795 if namespace
not in self
.delete_namespace_locks
:
796 self
.delete_namespace_locks
[namespace
] = asyncio
.Lock(loop
=self
.loop
)
797 delete_lock
= self
.delete_namespace_locks
[namespace
]
799 while delete_lock
.locked():
800 will_not_delete
= True
801 await asyncio
.sleep(0.1)
804 self
.log
.info("Namespace {} deleted by another worker.".format(namespace
))
808 async with delete_lock
:
809 libjuju
= await self
._get
_libjuju
(vca_id
)
812 if namespace
is None:
813 raise N2VCBadArgumentsException(
814 message
="namespace is mandatory", bad_args
=["namespace"]
823 ) = self
._get
_namespace
_components
(namespace
=namespace
)
824 if ns_id
is not None:
826 models
= await libjuju
.list_models(contains
=ns_id
)
828 await libjuju
.destroy_model(
829 model_name
=model
, total_timeout
=total_timeout
831 except Exception as e
:
832 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
834 message
="Error deleting namespace {} : {}".format(
839 raise N2VCBadArgumentsException(
840 message
="only ns_id is permitted to delete yet",
841 bad_args
=["namespace"],
843 except Exception as e
:
844 self
.log
.error(f
"Error deleting namespace {namespace} : {e}")
847 self
.delete_namespace_locks
.pop(namespace
)
848 self
.log
.info("Namespace {} deleted".format(namespace
))
850 async def delete_execution_environment(
853 db_dict
: dict = None,
854 total_timeout
: float = None,
855 scaling_in
: bool = False,
856 vca_type
: str = None,
860 Delete an execution environment
861 :param str ee_id: id of the execution environment to delete
862 :param dict db_dict: where to write into database when the status changes.
863 It contains a dict with
864 {collection: <str>, filter: {}, path: <str>},
865 e.g. {collection: "nsrs", filter:
866 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
867 :param: total_timeout: Total timeout
868 :param: scaling_in: Boolean to indicate if it is a scaling in operation
869 :param: vca_type: VCA type
870 :param: vca_id: VCA ID
872 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
873 libjuju
= await self
._get
_libjuju
(vca_id
)
877 raise N2VCBadArgumentsException(
878 message
="ee_id is mandatory", bad_args
=["ee_id"]
881 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
887 await libjuju
.destroy_model(
888 model_name
=model_name
,
889 total_timeout
=total_timeout
,
891 elif vca_type
== "native_charm" and scaling_in
:
892 # destroy the unit in the application
893 await libjuju
.destroy_unit(
894 application_name
=application_name
,
895 model_name
=model_name
,
896 machine_id
=machine_id
,
897 total_timeout
=total_timeout
,
900 # destroy the application
901 await libjuju
.destroy_application(
902 model_name
=model_name
,
903 application_name
=application_name
,
904 total_timeout
=total_timeout
,
906 except Exception as e
:
909 "Error deleting execution environment {} (application {}) : {}"
910 ).format(ee_id
, application_name
, e
)
913 self
.log
.info("Execution environment {} deleted".format(ee_id
))
915 async def exec_primitive(
920 db_dict
: dict = None,
921 progress_timeout
: float = None,
922 total_timeout
: float = None,
924 vca_type
: str = None,
927 Execute a primitive in the execution environment
929 :param: ee_id: the one returned by create_execution_environment or
930 register_execution_environment
931 :param: primitive_name: must be one defined in the software. There is one
932 called 'config', where, for the proxy case, the 'credentials' of VM are
934 :param: params_dict: parameters of the action
935 :param: db_dict: where to write into database when the status changes.
936 It contains a dict with
937 {collection: <str>, filter: {}, path: <str>},
938 e.g. {collection: "nsrs", filter:
939 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
940 :param: progress_timeout: Progress timeout
941 :param: total_timeout: Total timeout
942 :param: vca_id: VCA ID
943 :param: vca_type: VCA type
944 :returns str: primitive result, if ok. It raises exceptions in case of fail
948 "Executing primitive: {} on ee: {}, params: {}".format(
949 primitive_name
, ee_id
, params_dict
952 libjuju
= await self
._get
_libjuju
(vca_id
)
955 if ee_id
is None or len(ee_id
) == 0:
956 raise N2VCBadArgumentsException(
957 message
="ee_id is mandatory", bad_args
=["ee_id"]
959 if primitive_name
is None or len(primitive_name
) == 0:
960 raise N2VCBadArgumentsException(
961 message
="action_name is mandatory", bad_args
=["action_name"]
963 if params_dict
is None:
971 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
972 # To run action on the leader unit in libjuju.execute_action function,
973 # machine_id must be set to None if vca_type is not native_charm
974 if vca_type
!= "native_charm":
977 raise N2VCBadArgumentsException(
978 message
="ee_id={} is not a valid execution environment id".format(
984 if primitive_name
== "config":
985 # Special case: config primitive
987 await libjuju
.configure_application(
988 model_name
=model_name
,
989 application_name
=application_name
,
992 actions
= await libjuju
.get_actions(
993 application_name
=application_name
,
994 model_name
=model_name
,
997 "Application {} has these actions: {}".format(
998 application_name
, actions
1001 if "verify-ssh-credentials" in actions
:
1002 # execute verify-credentials
1004 retry_timeout
= 15.0
1005 for _
in range(num_retries
):
1007 self
.log
.debug("Executing action verify-ssh-credentials...")
1008 output
, ok
= await libjuju
.execute_action(
1009 model_name
=model_name
,
1010 application_name
=application_name
,
1011 action_name
="verify-ssh-credentials",
1013 progress_timeout
=progress_timeout
,
1014 total_timeout
=total_timeout
,
1019 "Error executing verify-ssh-credentials: {}. Retrying..."
1021 await asyncio
.sleep(retry_timeout
)
1024 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1026 except asyncio
.CancelledError
:
1030 "Error executing verify-ssh-credentials after {} retries. ".format(
1035 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
1038 self
.log
.debug(msg
=msg
)
1039 except Exception as e
:
1040 self
.log
.error("Error configuring juju application: {}".format(e
))
1041 raise N2VCExecutionException(
1042 message
="Error configuring application into ee={} : {}".format(
1045 primitive_name
=primitive_name
,
1050 output
, status
= await libjuju
.execute_action(
1051 model_name
=model_name
,
1052 application_name
=application_name
,
1053 action_name
=primitive_name
,
1055 machine_id
=machine_id
,
1056 progress_timeout
=progress_timeout
,
1057 total_timeout
=total_timeout
,
1060 if status
== "completed":
1063 raise Exception("status is not completed: {}".format(status
))
1064 except Exception as e
:
1066 "Error executing primitive {}: {}".format(primitive_name
, e
)
1068 raise N2VCExecutionException(
1069 message
="Error executing primitive {} into ee={} : {}".format(
1070 primitive_name
, ee_id
, e
1072 primitive_name
=primitive_name
,
1075 async def upgrade_charm(
1079 charm_id
: str = None,
1080 charm_type
: str = None,
1081 timeout
: float = None,
1083 """This method upgrade charms in VNFs
1086 ee_id: Execution environment id
1087 path: Local path to the charm
1089 charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm
1090 timeout: (Float) Timeout for the ns update operation
1093 The output of the update operation if status equals to "completed"
1096 self
.log
.info("Upgrading charm: {} on ee: {}".format(path
, ee_id
))
1097 libjuju
= await self
._get
_libjuju
(charm_id
)
1100 if ee_id
is None or len(ee_id
) == 0:
1101 raise N2VCBadArgumentsException(
1102 message
="ee_id is mandatory", bad_args
=["ee_id"]
1109 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
1112 raise N2VCBadArgumentsException(
1113 message
="ee_id={} is not a valid execution environment id".format(
1120 await libjuju
.upgrade_charm(
1121 application_name
=application_name
,
1123 model_name
=model_name
,
1124 total_timeout
=timeout
,
1127 return f
"Charm upgraded with application name {application_name}"
1129 except Exception as e
:
1130 self
.log
.error("Error upgrading charm {}: {}".format(path
, e
))
1132 raise N2VCException(
1133 message
="Error upgrading charm {} in ee={} : {}".format(path
, ee_id
, e
)
1136 async def disconnect(self
, vca_id
: str = None):
1140 :param: vca_id: VCA ID
1142 self
.log
.info("closing juju N2VC...")
1143 libjuju
= await self
._get
_libjuju
(vca_id
)
1145 await libjuju
.disconnect()
1146 except Exception as e
:
1147 raise N2VCConnectionException(
1148 message
="Error disconnecting controller: {}".format(e
),
1149 url
=libjuju
.vca_connection
.data
.endpoints
,
1153 ####################################################################################
1154 ################################### P R I V A T E ##################################
1155 ####################################################################################
1158 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1162 :param: vca_id: VCA ID
1163 If None, get a libjuju object with a Connection to the default VCA
1164 Else, geta libjuju object with a Connection to the specified VCA
1167 while self
.loading_libjuju
.locked():
1168 await asyncio
.sleep(0.1)
1169 if not self
.libjuju
:
1170 async with self
.loading_libjuju
:
1171 vca_connection
= await get_connection(self
._store
)
1172 self
.libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
)
1175 vca_connection
= await get_connection(self
._store
, vca_id
)
1183 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
1184 # write ee_id to database: _admin.deployed.VCA.x
1186 the_table
= db_dict
["collection"]
1187 the_filter
= db_dict
["filter"]
1188 the_path
= db_dict
["path"]
1189 if not the_path
[-1] == ".":
1190 the_path
= the_path
+ "."
1191 update_dict
= {the_path
+ "ee_id": ee_id
}
1192 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1195 q_filter
=the_filter
,
1196 update_dict
=update_dict
,
1199 except asyncio
.CancelledError
:
1201 except Exception as e
:
1202 self
.log
.error("Error writing ee_id to database: {}".format(e
))
1205 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
1207 Build an execution environment id form model, application and machine
1209 :param application_name:
1213 # id for the execution environment
1214 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1217 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1219 Get model, application and machine components from an execution environment id
1221 :return: model_name, application_name, machine_id
1224 return get_ee_id_components(ee_id
)
1226 def _get_application_name(self
, namespace
: str) -> str:
1228 Build application name from namespace
1230 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
1233 # TODO: Enforce the Juju 50-character application limit
1235 # split namespace components
1236 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
1240 if vnf_id
is None or len(vnf_id
) == 0:
1243 # Shorten the vnf_id to its last twelve characters
1244 vnf_id
= "vnf-" + vnf_id
[-12:]
1246 if vdu_id
is None or len(vdu_id
) == 0:
1249 # Shorten the vdu_id to its last twelve characters
1250 vdu_id
= "-vdu-" + vdu_id
[-12:]
1252 if vdu_count
is None or len(vdu_count
) == 0:
1255 vdu_count
= "-cnt-" + vdu_count
1257 # Generate a random suffix with 5 characters (the default size used by K8s)
1258 random_suffix
= generate_random_alfanum_string(size
=5)
1260 application_name
= "app-{}{}{}-{}".format(
1261 vnf_id
, vdu_id
, vdu_count
, random_suffix
1264 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1267 def _format_model_name(name
: str) -> str:
1268 """Format the name of the model.
1270 Model names may only contain lowercase letters, digits and hyphens
1273 return name
.replace("_", "-").replace(" ", "-").lower()
1276 def _format_app_name(name
: str) -> str:
1277 """Format the name of the application (in order to assure valid application name).
1279 Application names have restrictions (run juju deploy --help):
1280 - contains lowercase letters 'a'-'z'
1281 - contains numbers '0'-'9'
1282 - contains hyphens '-'
1283 - starts with a lowercase letter
1284 - not two or more consecutive hyphens
1285 - after a hyphen, not a group with all numbers
1288 def all_numbers(s
: str) -> bool:
1294 new_name
= name
.replace("_", "-")
1295 new_name
= new_name
.replace(" ", "-")
1296 new_name
= new_name
.lower()
1297 while new_name
.find("--") >= 0:
1298 new_name
= new_name
.replace("--", "-")
1299 groups
= new_name
.split("-")
1301 # find 'all numbers' groups and prefix them with a letter
1303 for i
in range(len(groups
)):
1305 if all_numbers(group
):
1311 if app_name
[0].isdigit():
1312 app_name
= "z" + app_name
1316 async def validate_vca(self
, vca_id
: str):
1318 Validate a VCA by connecting/disconnecting to/from it
1320 :param: vca_id: VCA ID
1322 vca_connection
= await get_connection(self
._store
, vca_id
=vca_id
)
1323 libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1324 controller
= await libjuju
.get_controller()
1325 await libjuju
.disconnect_controller(controller
)