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
,
32 N2VCApplicationExists
,
33 JujuApplicationExists
,
37 from n2vc
.n2vc_conn
import N2VCConnector
38 from n2vc
.n2vc_conn
import obj_to_dict
, obj_to_yaml
39 from n2vc
.libjuju
import Libjuju
40 from n2vc
.store
import MotorStore
41 from n2vc
.utils
import generate_random_alfanum_string
42 from n2vc
.vca
.connection
import get_connection
43 from retrying_async
import retry
46 class N2VCJujuConnector(N2VCConnector
):
49 ####################################################################################
50 ################################### P U B L I C ####################################
51 ####################################################################################
54 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
68 :param: db: Database object from osm_common
69 :param: fs: Filesystem object from osm_common
71 :param: loop: Asyncio loop
72 :param: on_update_db: Callback function to be called for updating the database.
75 # parent class constructor
76 N2VCConnector
.__init
__(
82 on_update_db
=on_update_db
,
85 # silence websocket traffic log
86 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
87 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
88 logging
.getLogger("model").setLevel(logging
.WARN
)
90 self
.log
.info("Initializing N2VC juju connector...")
92 db_uri
= EnvironConfig(prefixes
=["OSMLCM_", "OSMMON_"]).get("database_uri")
93 self
._store
= MotorStore(db_uri
)
94 self
.loading_libjuju
= asyncio
.Lock(loop
=self
.loop
)
95 self
.delete_namespace_locks
= {}
96 self
.log
.info("N2VC juju connector initialized")
99 self
, namespace
: str, yaml_format
: bool = True, vca_id
: str = None
102 Get status from all juju models from a VCA
104 :param namespace: we obtain ns from namespace
105 :param yaml_format: returns a yaml string
106 :param: vca_id: VCA ID from which the status will be retrieved.
108 # TODO: Review where is this function used. It is not optimal at all to get the status
109 # from all the juju models of a particular VCA. Additionally, these models might
110 # not have been deployed by OSM, in that case we are getting information from
111 # deployments outside of OSM's scope.
113 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
114 libjuju
= await self
._get
_libjuju
(vca_id
)
116 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
119 # model name is ns_id
121 if model_name
is None:
122 msg
= "Namespace {} not valid".format(namespace
)
124 raise N2VCBadArgumentsException(msg
, ["namespace"])
127 models
= await libjuju
.list_models(contains
=ns_id
)
130 status
[m
] = await libjuju
.get_model_status(m
)
133 return obj_to_yaml(status
)
135 return obj_to_dict(status
)
137 async def update_vca_status(self
, vcastatus
: dict, vca_id
: str = None):
139 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
141 :param vcastatus: dict containing vcaStatus
142 :param: vca_id: VCA ID
147 libjuju
= await self
._get
_libjuju
(vca_id
)
148 for model_name
in vcastatus
:
149 # Adding executed actions
150 vcastatus
[model_name
][
152 ] = await libjuju
.get_executed_actions(model_name
)
153 for application
in vcastatus
[model_name
]["applications"]:
154 # Adding application actions
155 vcastatus
[model_name
]["applications"][application
][
157 ] = await libjuju
.get_actions(application
, model_name
)
158 # Adding application configs
159 vcastatus
[model_name
]["applications"][application
][
161 ] = await libjuju
.get_application_configs(model_name
, application
)
162 except Exception as e
:
163 self
.log
.debug("Error in updating vca status: {}".format(str(e
)))
165 async def create_execution_environment(
169 reuse_ee_id
: str = None,
170 progress_timeout
: float = None,
171 total_timeout
: float = None,
175 Create an Execution Environment. Returns when it is created or raises an
178 :param: namespace: Contains a dot separate string.
179 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
180 :param: db_dict: where to write to database when the status changes.
181 It contains a dictionary with {collection: str, filter: {}, path: str},
182 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
183 "_admin.deployed.VCA.3"}
184 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
186 :param: progress_timeout: Progress timeout
187 :param: total_timeout: Total timeout
188 :param: vca_id: VCA ID
190 :returns: id of the new execution environment and credentials for it
191 (credentials can contains hostname, username, etc depending on underlying cloud)
195 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
196 namespace
, reuse_ee_id
199 libjuju
= await self
._get
_libjuju
(vca_id
)
203 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
213 ) = self
._get
_namespace
_components
(namespace
=namespace
)
214 # model name is ns_id
217 application_name
= self
._get
_application
_name
(namespace
=namespace
)
220 "model name: {}, application name: {}, machine_id: {}".format(
221 model_name
, application_name
, machine_id
225 # create or reuse a new juju machine
227 if not await libjuju
.model_exists(model_name
):
228 await libjuju
.add_model(
230 libjuju
.vca_connection
.lxd_cloud
,
232 machine
, new
= await libjuju
.create_machine(
233 model_name
=model_name
,
234 machine_id
=machine_id
,
236 progress_timeout
=progress_timeout
,
237 total_timeout
=total_timeout
,
239 # id for the execution environment
240 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
241 model_name
=model_name
,
242 application_name
=application_name
,
243 machine_id
=str(machine
.entity_id
),
245 self
.log
.debug("ee_id: {}".format(ee_id
))
248 # write ee_id in database
249 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
251 except Exception as e
:
252 message
= "Error creating machine on juju: {}".format(e
)
253 self
.log
.error(message
)
254 raise N2VCException(message
=message
)
256 # new machine credentials
258 "hostname": machine
.dns_name
,
262 "Execution environment created. ee_id: {}, credentials: {}".format(
267 return ee_id
, credentials
269 async def register_execution_environment(
274 progress_timeout
: float = None,
275 total_timeout
: float = None,
279 Register an existing execution environment at the VCA
281 :param: namespace: Contains a dot separate string.
282 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
283 :param: credentials: credentials to access the existing execution environment
284 (it can contains hostname, username, path to private key,
285 etc depending on underlying cloud)
286 :param: db_dict: where to write to database when the status changes.
287 It contains a dictionary with {collection: str, filter: {}, path: str},
288 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
289 "_admin.deployed.VCA.3"}
290 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
292 :param: progress_timeout: Progress timeout
293 :param: total_timeout: Total timeout
294 :param: vca_id: VCA ID
296 :returns: id of the execution environment
299 "Registering execution environment. namespace={}, credentials={}".format(
300 namespace
, credentials
303 libjuju
= await self
._get
_libjuju
(vca_id
)
305 if credentials
is None:
306 raise N2VCBadArgumentsException(
307 message
="credentials are mandatory", bad_args
=["credentials"]
309 if credentials
.get("hostname"):
310 hostname
= credentials
["hostname"]
312 raise N2VCBadArgumentsException(
313 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
315 if credentials
.get("username"):
316 username
= credentials
["username"]
318 raise N2VCBadArgumentsException(
319 message
="username is mandatory", bad_args
=["credentials.username"]
321 if "private_key_path" in credentials
:
322 private_key_path
= credentials
["private_key_path"]
324 # if not passed as argument, use generated private key path
325 private_key_path
= self
.private_key_path
327 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
334 application_name
= self
._get
_application
_name
(namespace
=namespace
)
336 # register machine on juju
338 if not await libjuju
.model_exists(model_name
):
339 await libjuju
.add_model(
341 libjuju
.vca_connection
.lxd_cloud
,
343 machine_id
= await libjuju
.provision_machine(
344 model_name
=model_name
,
347 private_key_path
=private_key_path
,
349 progress_timeout
=progress_timeout
,
350 total_timeout
=total_timeout
,
352 except Exception as e
:
353 self
.log
.error("Error registering machine: {}".format(e
))
355 message
="Error registering machine on juju: {}".format(e
)
358 self
.log
.info("Machine registered: {}".format(machine_id
))
360 # id for the execution environment
361 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
362 model_name
=model_name
,
363 application_name
=application_name
,
364 machine_id
=str(machine_id
),
367 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
371 # In case of native_charm is being deployed, if JujuApplicationExists error happens
372 # it will try to add_unit
373 @retry(attempts
=3, delay
=5, retry_exceptions
=(N2VCApplicationExists
,), timeout
=None)
374 async def install_configuration_sw(
379 progress_timeout
: float = None,
380 total_timeout
: float = None,
384 scaling_out
: bool = False,
385 vca_type
: str = None,
388 Install the software inside the execution environment identified by ee_id
390 :param: ee_id: the id of the execution environment returned by
391 create_execution_environment or register_execution_environment
392 :param: artifact_path: where to locate the artifacts (parent folder) using
394 the final artifact path will be a combination of this
395 artifact_path and additional string from the config_dict
397 :param: db_dict: where to write into database when the status changes.
398 It contains a dict with
399 {collection: <str>, filter: {}, path: <str>},
400 e.g. {collection: "nsrs", filter:
401 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
402 :param: progress_timeout: Progress timeout
403 :param: total_timeout: Total timeout
404 :param: config: Dictionary with deployment config information.
405 :param: num_units: Number of units to deploy of a particular charm.
406 :param: vca_id: VCA ID
407 :param: scaling_out: Boolean to indicate if it is a scaling out operation
408 :param: vca_type: VCA type
413 "Installing configuration sw on ee_id: {}, "
414 "artifact path: {}, db_dict: {}"
415 ).format(ee_id
, artifact_path
, db_dict
)
417 libjuju
= await self
._get
_libjuju
(vca_id
)
420 if ee_id
is None or len(ee_id
) == 0:
421 raise N2VCBadArgumentsException(
422 message
="ee_id is mandatory", bad_args
=["ee_id"]
424 if artifact_path
is None or len(artifact_path
) == 0:
425 raise N2VCBadArgumentsException(
426 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
429 raise N2VCBadArgumentsException(
430 message
="db_dict is mandatory", bad_args
=["db_dict"]
438 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
440 "model: {}, application: {}, machine: {}".format(
441 model_name
, application_name
, machine_id
445 raise N2VCBadArgumentsException(
446 message
="ee_id={} is not a valid execution environment id".format(
452 # remove // in charm path
453 while artifact_path
.find("//") >= 0:
454 artifact_path
= artifact_path
.replace("//", "/")
457 if not self
.fs
.file_exists(artifact_path
):
458 msg
= "artifact path does not exist: {}".format(artifact_path
)
459 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
461 if artifact_path
.startswith("/"):
462 full_path
= self
.fs
.path
+ artifact_path
464 full_path
= self
.fs
.path
+ "/" + artifact_path
467 if vca_type
== "native_charm" and await libjuju
.check_application_exists(
468 model_name
, application_name
470 await libjuju
.add_unit(
471 application_name
=application_name
,
472 model_name
=model_name
,
473 machine_id
=machine_id
,
475 progress_timeout
=progress_timeout
,
476 total_timeout
=total_timeout
,
479 await libjuju
.deploy_charm(
480 model_name
=model_name
,
481 application_name
=application_name
,
483 machine_id
=machine_id
,
485 progress_timeout
=progress_timeout
,
486 total_timeout
=total_timeout
,
490 except JujuApplicationExists
as e
:
491 raise N2VCApplicationExists(
492 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
.message
)
494 except Exception as e
:
496 message
="Error deploying charm into ee={} : {}".format(ee_id
, e
)
499 self
.log
.info("Configuration sw installed")
501 async def install_k8s_proxy_charm(
507 progress_timeout
: float = None,
508 total_timeout
: float = None,
513 Install a k8s proxy charm
515 :param charm_name: Name of the charm being deployed
516 :param namespace: collection of all the uuids related to the charm.
517 :param str artifact_path: where to locate the artifacts (parent folder) using
519 the final artifact path will be a combination of this artifact_path and
520 additional string from the config_dict (e.g. charm name)
521 :param dict db_dict: where to write into database when the status changes.
522 It contains a dict with
523 {collection: <str>, filter: {}, path: <str>},
524 e.g. {collection: "nsrs", filter:
525 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
526 :param: progress_timeout: Progress timeout
527 :param: total_timeout: Total timeout
528 :param config: Dictionary with additional configuration
529 :param vca_id: VCA ID
531 :returns ee_id: execution environment id.
534 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
535 charm_name
, artifact_path
, db_dict
538 libjuju
= await self
._get
_libjuju
(vca_id
)
540 if artifact_path
is None or len(artifact_path
) == 0:
541 raise N2VCBadArgumentsException(
542 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
545 raise N2VCBadArgumentsException(
546 message
="db_dict is mandatory", bad_args
=["db_dict"]
549 # remove // in charm path
550 while artifact_path
.find("//") >= 0:
551 artifact_path
= artifact_path
.replace("//", "/")
554 if not self
.fs
.file_exists(artifact_path
):
555 msg
= "artifact path does not exist: {}".format(artifact_path
)
556 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
558 if artifact_path
.startswith("/"):
559 full_path
= self
.fs
.path
+ artifact_path
561 full_path
= self
.fs
.path
+ "/" + artifact_path
563 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
564 model_name
= "{}-k8s".format(ns_id
)
565 if not await libjuju
.model_exists(model_name
):
566 await libjuju
.add_model(
568 libjuju
.vca_connection
.k8s_cloud
,
570 application_name
= self
._get
_application
_name
(namespace
)
573 await libjuju
.deploy_charm(
574 model_name
=model_name
,
575 application_name
=application_name
,
579 progress_timeout
=progress_timeout
,
580 total_timeout
=total_timeout
,
583 except Exception as e
:
584 raise N2VCException(message
="Error deploying charm: {}".format(e
))
586 self
.log
.info("K8s proxy charm installed")
587 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
588 model_name
=model_name
,
589 application_name
=application_name
,
593 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
597 async def get_ee_ssh_public__key(
601 progress_timeout
: float = None,
602 total_timeout
: float = None,
606 Get Execution environment ssh public key
608 :param: ee_id: the id of the execution environment returned by
609 create_execution_environment or register_execution_environment
610 :param: db_dict: where to write into database when the status changes.
611 It contains a dict with
612 {collection: <str>, filter: {}, path: <str>},
613 e.g. {collection: "nsrs", filter:
614 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
615 :param: progress_timeout: Progress timeout
616 :param: total_timeout: Total timeout
617 :param vca_id: VCA ID
618 :returns: public key of the execution environment
619 For the case of juju proxy charm ssh-layered, it is the one
620 returned by 'get-ssh-public-key' primitive.
621 It raises a N2VC exception if fails
626 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
627 ).format(ee_id
, db_dict
)
629 libjuju
= await self
._get
_libjuju
(vca_id
)
632 if ee_id
is None or len(ee_id
) == 0:
633 raise N2VCBadArgumentsException(
634 message
="ee_id is mandatory", bad_args
=["ee_id"]
637 raise N2VCBadArgumentsException(
638 message
="db_dict is mandatory", bad_args
=["db_dict"]
646 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
648 "model: {}, application: {}, machine: {}".format(
649 model_name
, application_name
, machine_id
653 raise N2VCBadArgumentsException(
654 message
="ee_id={} is not a valid execution environment id".format(
660 # try to execute ssh layer primitives (if exist):
666 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
668 # execute action: generate-ssh-key
670 output
, _status
= await libjuju
.execute_action(
671 model_name
=model_name
,
672 application_name
=application_name
,
673 action_name
="generate-ssh-key",
675 progress_timeout
=progress_timeout
,
676 total_timeout
=total_timeout
,
678 except Exception as e
:
680 "Skipping exception while executing action generate-ssh-key: {}".format(
685 # execute action: get-ssh-public-key
687 output
, _status
= await libjuju
.execute_action(
688 model_name
=model_name
,
689 application_name
=application_name
,
690 action_name
="get-ssh-public-key",
692 progress_timeout
=progress_timeout
,
693 total_timeout
=total_timeout
,
695 except Exception as e
:
696 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
698 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
700 # return public key if exists
701 return output
["pubkey"] if "pubkey" in output
else output
703 async def get_metrics(
704 self
, model_name
: str, application_name
: str, vca_id
: str = None
707 Get metrics from application
709 :param: model_name: Model name
710 :param: application_name: Application name
711 :param: vca_id: VCA ID
713 :return: Dictionary with obtained metrics
715 libjuju
= await self
._get
_libjuju
(vca_id
)
716 return await libjuju
.get_metrics(model_name
, application_name
)
718 async def add_relation(
727 Add relation between two charmed endpoints
729 :param: ee_id_1: The id of the first execution environment
730 :param: ee_id_2: The id of the second execution environment
731 :param: endpoint_1: The endpoint in the first execution environment
732 :param: endpoint_2: The endpoint in the second execution environment
733 :param: vca_id: VCA ID
736 "adding new relation between {} and {}, endpoints: {}, {}".format(
737 ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
740 libjuju
= await self
._get
_libjuju
(vca_id
)
744 message
= "EE 1 is mandatory"
745 self
.log
.error(message
)
746 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_1"])
748 message
= "EE 2 is mandatory"
749 self
.log
.error(message
)
750 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_2"])
752 message
= "endpoint 1 is mandatory"
753 self
.log
.error(message
)
754 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_1"])
756 message
= "endpoint 2 is mandatory"
757 self
.log
.error(message
)
758 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_2"])
760 # get the model, the applications and the machines from the ee_id's
761 model_1
, app_1
, _machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
762 model_2
, app_2
, _machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
764 # model must be the same
765 if model_1
!= model_2
:
766 message
= "EE models are not the same: {} vs {}".format(ee_id_1
, ee_id_2
)
767 self
.log
.error(message
)
768 raise N2VCBadArgumentsException(
769 message
=message
, bad_args
=["ee_id_1", "ee_id_2"]
772 # add juju relations between two applications
774 await libjuju
.add_relation(
776 endpoint_1
="{}:{}".format(app_1
, endpoint_1
),
777 endpoint_2
="{}:{}".format(app_2
, endpoint_2
),
779 except Exception as e
:
780 message
= "Error adding relation between {} and {}: {}".format(
783 self
.log
.error(message
)
784 raise N2VCException(message
=message
)
786 async def remove_relation(self
):
788 self
.log
.info("Method not implemented yet")
789 raise MethodNotImplemented()
791 async def deregister_execution_environments(self
):
792 self
.log
.info("Method not implemented yet")
793 raise MethodNotImplemented()
795 async def delete_namespace(
798 db_dict
: dict = None,
799 total_timeout
: float = None,
803 Remove a network scenario and its execution environments
804 :param: namespace: [<nsi-id>].<ns-id>
805 :param: db_dict: where to write into database when the status changes.
806 It contains a dict with
807 {collection: <str>, filter: {}, path: <str>},
808 e.g. {collection: "nsrs", filter:
809 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
810 :param: total_timeout: Total timeout
811 :param: vca_id: VCA ID
813 self
.log
.info("Deleting namespace={}".format(namespace
))
814 will_not_delete
= False
815 if namespace
not in self
.delete_namespace_locks
:
816 self
.delete_namespace_locks
[namespace
] = asyncio
.Lock(loop
=self
.loop
)
817 delete_lock
= self
.delete_namespace_locks
[namespace
]
819 while delete_lock
.locked():
820 will_not_delete
= True
821 await asyncio
.sleep(0.1)
824 self
.log
.info("Namespace {} deleted by another worker.".format(namespace
))
828 async with delete_lock
:
829 libjuju
= await self
._get
_libjuju
(vca_id
)
832 if namespace
is None:
833 raise N2VCBadArgumentsException(
834 message
="namespace is mandatory", bad_args
=["namespace"]
843 ) = self
._get
_namespace
_components
(namespace
=namespace
)
844 if ns_id
is not None:
846 models
= await libjuju
.list_models(contains
=ns_id
)
848 await libjuju
.destroy_model(
849 model_name
=model
, total_timeout
=total_timeout
851 except Exception as e
:
853 message
="Error deleting namespace {} : {}".format(
858 raise N2VCBadArgumentsException(
859 message
="only ns_id is permitted to delete yet",
860 bad_args
=["namespace"],
863 self
.delete_namespace_locks
.pop(namespace
)
864 self
.log
.info("Namespace {} deleted".format(namespace
))
866 async def delete_execution_environment(
869 db_dict
: dict = None,
870 total_timeout
: float = None,
871 scaling_in
: bool = False,
872 vca_type
: str = None,
876 Delete an execution environment
877 :param str ee_id: id of the execution environment to delete
878 :param dict db_dict: where to write into database when the status changes.
879 It contains a dict with
880 {collection: <str>, filter: {}, path: <str>},
881 e.g. {collection: "nsrs", filter:
882 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
883 :param: total_timeout: Total timeout
884 :param: scaling_in: Boolean to indicate if it is a scaling in operation
885 :param: vca_type: VCA type
886 :param: vca_id: VCA ID
888 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
889 libjuju
= await self
._get
_libjuju
(vca_id
)
893 raise N2VCBadArgumentsException(
894 message
="ee_id is mandatory", bad_args
=["ee_id"]
897 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
903 await libjuju
.destroy_model(
904 model_name
=model_name
,
905 total_timeout
=total_timeout
,
907 elif vca_type
== "native_charm" and scaling_in
:
908 # destroy the unit in the application
909 await libjuju
.destroy_unit(
910 application_name
=application_name
,
911 model_name
=model_name
,
912 machine_id
=machine_id
,
913 total_timeout
=total_timeout
,
916 # destroy the application
917 await libjuju
.destroy_application(
918 model_name
=model_name
,
919 application_name
=application_name
,
920 total_timeout
=total_timeout
,
922 except Exception as e
:
925 "Error deleting execution environment {} (application {}) : {}"
926 ).format(ee_id
, application_name
, e
)
929 self
.log
.info("Execution environment {} deleted".format(ee_id
))
931 async def exec_primitive(
936 db_dict
: dict = None,
937 progress_timeout
: float = None,
938 total_timeout
: float = None,
940 vca_type
: str = None,
943 Execute a primitive in the execution environment
945 :param: ee_id: the one returned by create_execution_environment or
946 register_execution_environment
947 :param: primitive_name: must be one defined in the software. There is one
948 called 'config', where, for the proxy case, the 'credentials' of VM are
950 :param: params_dict: parameters of the action
951 :param: db_dict: where to write into database when the status changes.
952 It contains a dict with
953 {collection: <str>, filter: {}, path: <str>},
954 e.g. {collection: "nsrs", filter:
955 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
956 :param: progress_timeout: Progress timeout
957 :param: total_timeout: Total timeout
958 :param: vca_id: VCA ID
959 :param: vca_type: VCA type
960 :returns str: primitive result, if ok. It raises exceptions in case of fail
964 "Executing primitive: {} on ee: {}, params: {}".format(
965 primitive_name
, ee_id
, params_dict
968 libjuju
= await self
._get
_libjuju
(vca_id
)
971 if ee_id
is None or len(ee_id
) == 0:
972 raise N2VCBadArgumentsException(
973 message
="ee_id is mandatory", bad_args
=["ee_id"]
975 if primitive_name
is None or len(primitive_name
) == 0:
976 raise N2VCBadArgumentsException(
977 message
="action_name is mandatory", bad_args
=["action_name"]
979 if params_dict
is None:
987 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
988 # To run action on the leader unit in libjuju.execute_action function,
989 # machine_id must be set to None if vca_type is not native_charm
990 if vca_type
!= "native_charm":
993 raise N2VCBadArgumentsException(
994 message
="ee_id={} is not a valid execution environment id".format(
1000 if primitive_name
== "config":
1001 # Special case: config primitive
1003 await libjuju
.configure_application(
1004 model_name
=model_name
,
1005 application_name
=application_name
,
1008 actions
= await libjuju
.get_actions(
1009 application_name
=application_name
,
1010 model_name
=model_name
,
1013 "Application {} has these actions: {}".format(
1014 application_name
, actions
1017 if "verify-ssh-credentials" in actions
:
1018 # execute verify-credentials
1020 retry_timeout
= 15.0
1021 for _
in range(num_retries
):
1023 self
.log
.debug("Executing action verify-ssh-credentials...")
1024 output
, ok
= await libjuju
.execute_action(
1025 model_name
=model_name
,
1026 application_name
=application_name
,
1027 action_name
="verify-ssh-credentials",
1029 progress_timeout
=progress_timeout
,
1030 total_timeout
=total_timeout
,
1035 "Error executing verify-ssh-credentials: {}. Retrying..."
1037 await asyncio
.sleep(retry_timeout
)
1040 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1042 except asyncio
.CancelledError
:
1046 "Error executing verify-ssh-credentials after {} retries. ".format(
1051 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
1054 self
.log
.debug(msg
=msg
)
1055 except Exception as e
:
1056 self
.log
.error("Error configuring juju application: {}".format(e
))
1057 raise N2VCExecutionException(
1058 message
="Error configuring application into ee={} : {}".format(
1061 primitive_name
=primitive_name
,
1066 output
, status
= await libjuju
.execute_action(
1067 model_name
=model_name
,
1068 application_name
=application_name
,
1069 action_name
=primitive_name
,
1071 machine_id
=machine_id
,
1072 progress_timeout
=progress_timeout
,
1073 total_timeout
=total_timeout
,
1076 if status
== "completed":
1079 raise Exception("status is not completed: {}".format(status
))
1080 except Exception as e
:
1082 "Error executing primitive {}: {}".format(primitive_name
, e
)
1084 raise N2VCExecutionException(
1085 message
="Error executing primitive {} into ee={} : {}".format(
1086 primitive_name
, ee_id
, e
1088 primitive_name
=primitive_name
,
1091 async def disconnect(self
, vca_id
: str = None):
1095 :param: vca_id: VCA ID
1097 self
.log
.info("closing juju N2VC...")
1098 libjuju
= await self
._get
_libjuju
(vca_id
)
1100 await libjuju
.disconnect()
1101 except Exception as e
:
1102 raise N2VCConnectionException(
1103 message
="Error disconnecting controller: {}".format(e
),
1104 url
=libjuju
.vca_connection
.data
.endpoints
,
1108 ####################################################################################
1109 ################################### P R I V A T E ##################################
1110 ####################################################################################
1113 async def _get_libjuju(self
, vca_id
: str = None) -> Libjuju
:
1117 :param: vca_id: VCA ID
1118 If None, get a libjuju object with a Connection to the default VCA
1119 Else, geta libjuju object with a Connection to the specified VCA
1122 while self
.loading_libjuju
.locked():
1123 await asyncio
.sleep(0.1)
1124 if not self
.libjuju
:
1125 async with self
.loading_libjuju
:
1126 vca_connection
= await get_connection(self
._store
)
1127 self
.libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
)
1130 vca_connection
= await get_connection(self
._store
, vca_id
)
1138 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
1139 # write ee_id to database: _admin.deployed.VCA.x
1141 the_table
= db_dict
["collection"]
1142 the_filter
= db_dict
["filter"]
1143 the_path
= db_dict
["path"]
1144 if not the_path
[-1] == ".":
1145 the_path
= the_path
+ "."
1146 update_dict
= {the_path
+ "ee_id": ee_id
}
1147 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1150 q_filter
=the_filter
,
1151 update_dict
=update_dict
,
1154 except asyncio
.CancelledError
:
1156 except Exception as e
:
1157 self
.log
.error("Error writing ee_id to database: {}".format(e
))
1160 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
1162 Build an execution environment id form model, application and machine
1164 :param application_name:
1168 # id for the execution environment
1169 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1172 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1174 Get model, application and machine components from an execution environment id
1176 :return: model_name, application_name, machine_id
1180 return None, None, None
1182 # split components of id
1183 parts
= ee_id
.split(".")
1184 model_name
= parts
[0]
1185 application_name
= parts
[1]
1186 machine_id
= parts
[2]
1187 return model_name
, application_name
, machine_id
1189 def _get_application_name(self
, namespace
: str) -> str:
1191 Build application name from namespace
1193 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
1196 # TODO: Enforce the Juju 50-character application limit
1198 # split namespace components
1199 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
1203 if vnf_id
is None or len(vnf_id
) == 0:
1206 # Shorten the vnf_id to its last twelve characters
1207 vnf_id
= "vnf-" + vnf_id
[-12:]
1209 if vdu_id
is None or len(vdu_id
) == 0:
1212 # Shorten the vdu_id to its last twelve characters
1213 vdu_id
= "-vdu-" + vdu_id
[-12:]
1215 if vdu_count
is None or len(vdu_count
) == 0:
1218 vdu_count
= "-cnt-" + vdu_count
1220 # Generate a random suffix with 5 characters (the default size used by K8s)
1221 random_suffix
= generate_random_alfanum_string(size
=5)
1223 application_name
= "app-{}{}{}-{}".format(
1224 vnf_id
, vdu_id
, vdu_count
, random_suffix
1227 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1230 def _format_model_name(name
: str) -> str:
1231 """Format the name of the model.
1233 Model names may only contain lowercase letters, digits and hyphens
1236 return name
.replace("_", "-").replace(" ", "-").lower()
1239 def _format_app_name(name
: str) -> str:
1240 """Format the name of the application (in order to assure valid application name).
1242 Application names have restrictions (run juju deploy --help):
1243 - contains lowercase letters 'a'-'z'
1244 - contains numbers '0'-'9'
1245 - contains hyphens '-'
1246 - starts with a lowercase letter
1247 - not two or more consecutive hyphens
1248 - after a hyphen, not a group with all numbers
1251 def all_numbers(s
: str) -> bool:
1257 new_name
= name
.replace("_", "-")
1258 new_name
= new_name
.replace(" ", "-")
1259 new_name
= new_name
.lower()
1260 while new_name
.find("--") >= 0:
1261 new_name
= new_name
.replace("--", "-")
1262 groups
= new_name
.split("-")
1264 # find 'all numbers' groups and prefix them with a letter
1266 for i
in range(len(groups
)):
1268 if all_numbers(group
):
1274 if app_name
[0].isdigit():
1275 app_name
= "z" + app_name
1279 async def validate_vca(self
, vca_id
: str):
1281 Validate a VCA by connecting/disconnecting to/from it
1283 :param: vca_id: VCA ID
1285 vca_connection
= await get_connection(self
._store
, vca_id
=vca_id
)
1286 libjuju
= Libjuju(vca_connection
, loop
=self
.loop
, log
=self
.log
, n2vc
=self
)
1287 controller
= await libjuju
.get_controller()
1288 await libjuju
.disconnect_controller(controller
)