feature: helm charts repos with certs
[osm/N2VC.git] / n2vc / n2vc_juju_conn.py
1 ##
2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
4 # All Rights Reserved.
5 #
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
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
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
15 # implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
21 ##
22
23 import asyncio
24 import logging
25
26 from n2vc.config import EnvironConfig
27 from n2vc.definitions import RelationEndpoint
28 from n2vc.exceptions import (
29 N2VCBadArgumentsException,
30 N2VCException,
31 N2VCConnectionException,
32 N2VCExecutionException,
33 N2VCApplicationExists,
34 JujuApplicationExists,
35 # N2VCNotFound,
36 MethodNotImplemented,
37 )
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
43 from n2vc.vca.connection import get_connection
44 from retrying_async import retry
45
46
47 class N2VCJujuConnector(N2VCConnector):
48
49 """
50 ####################################################################################
51 ################################### P U B L I C ####################################
52 ####################################################################################
53 """
54
55 BUILT_IN_CLOUDS = ["localhost", "microk8s"]
56 libjuju = None
57
58 def __init__(
59 self,
60 db: object,
61 fs: object,
62 log: object = None,
63 loop: object = None,
64 on_update_db=None,
65 ):
66 """
67 Constructor
68
69 :param: db: Database object from osm_common
70 :param: fs: Filesystem object from osm_common
71 :param: log: Logger
72 :param: loop: Asyncio loop
73 :param: on_update_db: Callback function to be called for updating the database.
74 """
75
76 # parent class constructor
77 N2VCConnector.__init__(
78 self,
79 db=db,
80 fs=fs,
81 log=log,
82 loop=loop,
83 on_update_db=on_update_db,
84 )
85
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)
90
91 self.log.info("Initializing N2VC juju connector...")
92
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
97 self.log.info("N2VC juju connector initialized")
98
99 async def get_status(
100 self, namespace: str, yaml_format: bool = True, vca_id: str = None
101 ):
102 """
103 Get status from all juju models from a VCA
104
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.
108 """
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.
113
114 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
115 libjuju = await self._get_libjuju(vca_id)
116
117 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
118 namespace=namespace
119 )
120 # model name is ns_id
121 model_name = ns_id
122 if model_name is None:
123 msg = "Namespace {} not valid".format(namespace)
124 self.log.error(msg)
125 raise N2VCBadArgumentsException(msg, ["namespace"])
126
127 status = {}
128 models = await libjuju.list_models(contains=ns_id)
129
130 for m in models:
131 status[m] = await libjuju.get_model_status(m)
132
133 if yaml_format:
134 return obj_to_yaml(status)
135 else:
136 return obj_to_dict(status)
137
138 async def update_vca_status(self, vcastatus: dict, vca_id: str = None):
139 """
140 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
141
142 :param vcastatus: dict containing vcaStatus
143 :param: vca_id: VCA ID
144
145 :return: None
146 """
147 try:
148 libjuju = await self._get_libjuju(vca_id)
149 for model_name in vcastatus:
150 # Adding executed actions
151 vcastatus[model_name][
152 "executedActions"
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][
157 "actions"
158 ] = await libjuju.get_actions(application, model_name)
159 # Adding application configs
160 vcastatus[model_name]["applications"][application][
161 "configs"
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)))
165
166 async def create_execution_environment(
167 self,
168 namespace: str,
169 db_dict: dict,
170 reuse_ee_id: str = None,
171 progress_timeout: float = None,
172 total_timeout: float = None,
173 vca_id: str = None,
174 ) -> (str, dict):
175 """
176 Create an Execution Environment. Returns when it is created or raises an
177 exception on failing
178
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
186 older environment
187 :param: progress_timeout: Progress timeout
188 :param: total_timeout: Total timeout
189 :param: vca_id: VCA ID
190
191 :returns: id of the new execution environment and credentials for it
192 (credentials can contains hostname, username, etc depending on underlying cloud)
193 """
194
195 self.log.info(
196 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
197 namespace, reuse_ee_id
198 )
199 )
200 libjuju = await self._get_libjuju(vca_id)
201
202 machine_id = None
203 if reuse_ee_id:
204 model_name, application_name, machine_id = self._get_ee_id_components(
205 ee_id=reuse_ee_id
206 )
207 else:
208 (
209 _nsi_id,
210 ns_id,
211 _vnf_id,
212 _vdu_id,
213 _vdu_count,
214 ) = self._get_namespace_components(namespace=namespace)
215 # model name is ns_id
216 model_name = ns_id
217 # application name
218 application_name = self._get_application_name(namespace=namespace)
219
220 self.log.debug(
221 "model name: {}, application name: {}, machine_id: {}".format(
222 model_name, application_name, machine_id
223 )
224 )
225
226 # create or reuse a new juju machine
227 try:
228 if not await libjuju.model_exists(model_name):
229 await libjuju.add_model(
230 model_name,
231 libjuju.vca_connection.lxd_cloud,
232 )
233 machine, new = await libjuju.create_machine(
234 model_name=model_name,
235 machine_id=machine_id,
236 db_dict=db_dict,
237 progress_timeout=progress_timeout,
238 total_timeout=total_timeout,
239 )
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),
245 )
246 self.log.debug("ee_id: {}".format(ee_id))
247
248 if new:
249 # write ee_id in database
250 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
251
252 except Exception as e:
253 message = "Error creating machine on juju: {}".format(e)
254 self.log.error(message)
255 raise N2VCException(message=message)
256
257 # new machine credentials
258 credentials = {
259 "hostname": machine.dns_name,
260 }
261
262 self.log.info(
263 "Execution environment created. ee_id: {}, credentials: {}".format(
264 ee_id, credentials
265 )
266 )
267
268 return ee_id, credentials
269
270 async def register_execution_environment(
271 self,
272 namespace: str,
273 credentials: dict,
274 db_dict: dict,
275 progress_timeout: float = None,
276 total_timeout: float = None,
277 vca_id: str = None,
278 ) -> str:
279 """
280 Register an existing execution environment at the VCA
281
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
292 older environment
293 :param: progress_timeout: Progress timeout
294 :param: total_timeout: Total timeout
295 :param: vca_id: VCA ID
296
297 :returns: id of the execution environment
298 """
299 self.log.info(
300 "Registering execution environment. namespace={}, credentials={}".format(
301 namespace, credentials
302 )
303 )
304 libjuju = await self._get_libjuju(vca_id)
305
306 if credentials is None:
307 raise N2VCBadArgumentsException(
308 message="credentials are mandatory", bad_args=["credentials"]
309 )
310 if credentials.get("hostname"):
311 hostname = credentials["hostname"]
312 else:
313 raise N2VCBadArgumentsException(
314 message="hostname is mandatory", bad_args=["credentials.hostname"]
315 )
316 if credentials.get("username"):
317 username = credentials["username"]
318 else:
319 raise N2VCBadArgumentsException(
320 message="username is mandatory", bad_args=["credentials.username"]
321 )
322 if "private_key_path" in credentials:
323 private_key_path = credentials["private_key_path"]
324 else:
325 # if not passed as argument, use generated private key path
326 private_key_path = self.private_key_path
327
328 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
329 namespace=namespace
330 )
331
332 # model name
333 model_name = ns_id
334 # application name
335 application_name = self._get_application_name(namespace=namespace)
336
337 # register machine on juju
338 try:
339 if not await libjuju.model_exists(model_name):
340 await libjuju.add_model(
341 model_name,
342 libjuju.vca_connection.lxd_cloud,
343 )
344 machine_id = await libjuju.provision_machine(
345 model_name=model_name,
346 hostname=hostname,
347 username=username,
348 private_key_path=private_key_path,
349 db_dict=db_dict,
350 progress_timeout=progress_timeout,
351 total_timeout=total_timeout,
352 )
353 except Exception as e:
354 self.log.error("Error registering machine: {}".format(e))
355 raise N2VCException(
356 message="Error registering machine on juju: {}".format(e)
357 )
358
359 self.log.info("Machine registered: {}".format(machine_id))
360
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),
366 )
367
368 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
369
370 return ee_id
371
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(
376 self,
377 ee_id: str,
378 artifact_path: str,
379 db_dict: dict,
380 progress_timeout: float = None,
381 total_timeout: float = None,
382 config: dict = None,
383 num_units: int = 1,
384 vca_id: str = None,
385 scaling_out: bool = False,
386 vca_type: str = None,
387 ):
388 """
389 Install the software inside the execution environment identified by ee_id
390
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
394 the self.fs
395 the final artifact path will be a combination of this
396 artifact_path and additional string from the config_dict
397 (e.g. charm name)
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
410 """
411
412 self.log.info(
413 (
414 "Installing configuration sw on ee_id: {}, "
415 "artifact path: {}, db_dict: {}"
416 ).format(ee_id, artifact_path, db_dict)
417 )
418 libjuju = await self._get_libjuju(vca_id)
419
420 # check arguments
421 if ee_id is None or len(ee_id) == 0:
422 raise N2VCBadArgumentsException(
423 message="ee_id is mandatory", bad_args=["ee_id"]
424 )
425 if artifact_path is None or len(artifact_path) == 0:
426 raise N2VCBadArgumentsException(
427 message="artifact_path is mandatory", bad_args=["artifact_path"]
428 )
429 if db_dict is None:
430 raise N2VCBadArgumentsException(
431 message="db_dict is mandatory", bad_args=["db_dict"]
432 )
433
434 try:
435 (
436 model_name,
437 application_name,
438 machine_id,
439 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
440 self.log.debug(
441 "model: {}, application: {}, machine: {}".format(
442 model_name, application_name, machine_id
443 )
444 )
445 except Exception:
446 raise N2VCBadArgumentsException(
447 message="ee_id={} is not a valid execution environment id".format(
448 ee_id
449 ),
450 bad_args=["ee_id"],
451 )
452
453 # remove // in charm path
454 while artifact_path.find("//") >= 0:
455 artifact_path = artifact_path.replace("//", "/")
456
457 # check charm path
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"])
461
462 if artifact_path.startswith("/"):
463 full_path = self.fs.path + artifact_path
464 else:
465 full_path = self.fs.path + "/" + artifact_path
466
467 try:
468 if vca_type == "native_charm" and await libjuju.check_application_exists(
469 model_name, application_name
470 ):
471 await libjuju.add_unit(
472 application_name=application_name,
473 model_name=model_name,
474 machine_id=machine_id,
475 db_dict=db_dict,
476 progress_timeout=progress_timeout,
477 total_timeout=total_timeout,
478 )
479 else:
480 await libjuju.deploy_charm(
481 model_name=model_name,
482 application_name=application_name,
483 path=full_path,
484 machine_id=machine_id,
485 db_dict=db_dict,
486 progress_timeout=progress_timeout,
487 total_timeout=total_timeout,
488 config=config,
489 num_units=num_units,
490 )
491 except JujuApplicationExists as e:
492 raise N2VCApplicationExists(
493 message="Error deploying charm into ee={} : {}".format(ee_id, e.message)
494 )
495 except Exception as e:
496 raise N2VCException(
497 message="Error deploying charm into ee={} : {}".format(ee_id, e)
498 )
499
500 self.log.info("Configuration sw installed")
501
502 async def install_k8s_proxy_charm(
503 self,
504 charm_name: str,
505 namespace: str,
506 artifact_path: str,
507 db_dict: dict,
508 progress_timeout: float = None,
509 total_timeout: float = None,
510 config: dict = None,
511 vca_id: str = None,
512 ) -> str:
513 """
514 Install a k8s proxy charm
515
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
519 the self.fs
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
531
532 :returns ee_id: execution environment id.
533 """
534 self.log.info(
535 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
536 charm_name, artifact_path, db_dict
537 )
538 )
539 libjuju = await self._get_libjuju(vca_id)
540
541 if artifact_path is None or len(artifact_path) == 0:
542 raise N2VCBadArgumentsException(
543 message="artifact_path is mandatory", bad_args=["artifact_path"]
544 )
545 if db_dict is None:
546 raise N2VCBadArgumentsException(
547 message="db_dict is mandatory", bad_args=["db_dict"]
548 )
549
550 # remove // in charm path
551 while artifact_path.find("//") >= 0:
552 artifact_path = artifact_path.replace("//", "/")
553
554 # check charm path
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"])
558
559 if artifact_path.startswith("/"):
560 full_path = self.fs.path + artifact_path
561 else:
562 full_path = self.fs.path + "/" + artifact_path
563
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(
568 model_name,
569 libjuju.vca_connection.k8s_cloud,
570 )
571 application_name = self._get_application_name(namespace)
572
573 try:
574 await libjuju.deploy_charm(
575 model_name=model_name,
576 application_name=application_name,
577 path=full_path,
578 machine_id=None,
579 db_dict=db_dict,
580 progress_timeout=progress_timeout,
581 total_timeout=total_timeout,
582 config=config,
583 )
584 except Exception as e:
585 raise N2VCException(message="Error deploying charm: {}".format(e))
586
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,
591 machine_id="k8s",
592 )
593
594 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
595
596 return ee_id
597
598 async def get_ee_ssh_public__key(
599 self,
600 ee_id: str,
601 db_dict: dict,
602 progress_timeout: float = None,
603 total_timeout: float = None,
604 vca_id: str = None,
605 ) -> str:
606 """
607 Get Execution environment ssh public key
608
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
623 """
624
625 self.log.info(
626 (
627 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
628 ).format(ee_id, db_dict)
629 )
630 libjuju = await self._get_libjuju(vca_id)
631
632 # check arguments
633 if ee_id is None or len(ee_id) == 0:
634 raise N2VCBadArgumentsException(
635 message="ee_id is mandatory", bad_args=["ee_id"]
636 )
637 if db_dict is None:
638 raise N2VCBadArgumentsException(
639 message="db_dict is mandatory", bad_args=["db_dict"]
640 )
641
642 try:
643 (
644 model_name,
645 application_name,
646 machine_id,
647 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
648 self.log.debug(
649 "model: {}, application: {}, machine: {}".format(
650 model_name, application_name, machine_id
651 )
652 )
653 except Exception:
654 raise N2VCBadArgumentsException(
655 message="ee_id={} is not a valid execution environment id".format(
656 ee_id
657 ),
658 bad_args=["ee_id"],
659 )
660
661 # try to execute ssh layer primitives (if exist):
662 # generate-ssh-key
663 # get-ssh-public-key
664
665 output = None
666
667 application_name = N2VCJujuConnector._format_app_name(application_name)
668
669 # execute action: generate-ssh-key
670 try:
671 output, _status = await libjuju.execute_action(
672 model_name=model_name,
673 application_name=application_name,
674 action_name="generate-ssh-key",
675 db_dict=db_dict,
676 progress_timeout=progress_timeout,
677 total_timeout=total_timeout,
678 )
679 except Exception as e:
680 self.log.info(
681 "Skipping exception while executing action generate-ssh-key: {}".format(
682 e
683 )
684 )
685
686 # execute action: get-ssh-public-key
687 try:
688 output, _status = await libjuju.execute_action(
689 model_name=model_name,
690 application_name=application_name,
691 action_name="get-ssh-public-key",
692 db_dict=db_dict,
693 progress_timeout=progress_timeout,
694 total_timeout=total_timeout,
695 )
696 except Exception as e:
697 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
698 self.log.info(msg)
699 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
700
701 # return public key if exists
702 return output["pubkey"] if "pubkey" in output else output
703
704 async def get_metrics(
705 self, model_name: str, application_name: str, vca_id: str = None
706 ) -> dict:
707 """
708 Get metrics from application
709
710 :param: model_name: Model name
711 :param: application_name: Application name
712 :param: vca_id: VCA ID
713
714 :return: Dictionary with obtained metrics
715 """
716 libjuju = await self._get_libjuju(vca_id)
717 return await libjuju.get_metrics(model_name, application_name)
718
719 async def add_relation(
720 self,
721 provider: RelationEndpoint,
722 requirer: RelationEndpoint,
723 ):
724 """
725 Add relation between two charmed endpoints
726
727 :param: provider: Provider relation endpoint
728 :param: requirer: Requirer relation endpoint
729 """
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
734 )
735 try:
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)
741 if offer:
742 saas_name = await requirer_libjuju.consume(
743 requirer.model_name, offer, provider_libjuju
744 )
745 await requirer_libjuju.add_relation(
746 requirer.model_name,
747 requirer.endpoint,
748 saas_name,
749 )
750 else:
751 # Standard 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(
757 model_name=model,
758 endpoint_1=provider.endpoint,
759 endpoint_2=requirer.endpoint,
760 )
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)
765
766 async def remove_relation(self):
767 # TODO
768 self.log.info("Method not implemented yet")
769 raise MethodNotImplemented()
770
771 async def deregister_execution_environments(self):
772 self.log.info("Method not implemented yet")
773 raise MethodNotImplemented()
774
775 async def delete_namespace(
776 self,
777 namespace: str,
778 db_dict: dict = None,
779 total_timeout: float = None,
780 vca_id: str = None,
781 ):
782 """
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
792 """
793 self.log.info("Deleting namespace={}".format(namespace))
794 libjuju = await self._get_libjuju(vca_id)
795
796 # check arguments
797 if namespace is None:
798 raise N2VCBadArgumentsException(
799 message="namespace is mandatory", bad_args=["namespace"]
800 )
801
802 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
803 namespace=namespace
804 )
805 if ns_id is not None:
806 try:
807 models = await libjuju.list_models(contains=ns_id)
808 for model in models:
809 await libjuju.destroy_model(
810 model_name=model, total_timeout=total_timeout
811 )
812 except Exception as e:
813 raise N2VCException(
814 message="Error deleting namespace {} : {}".format(namespace, e)
815 )
816 else:
817 raise N2VCBadArgumentsException(
818 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
819 )
820
821 self.log.info("Namespace {} deleted".format(namespace))
822
823 async def delete_execution_environment(
824 self,
825 ee_id: str,
826 db_dict: dict = None,
827 total_timeout: float = None,
828 scaling_in: bool = False,
829 vca_type: str = None,
830 vca_id: str = None,
831 ):
832 """
833 Delete an execution environment
834 :param str ee_id: id of the execution environment to delete
835 :param dict db_dict: where to write into database when the status changes.
836 It contains a dict with
837 {collection: <str>, filter: {}, path: <str>},
838 e.g. {collection: "nsrs", filter:
839 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
840 :param: total_timeout: Total timeout
841 :param: scaling_in: Boolean to indicate if it is a scaling in operation
842 :param: vca_type: VCA type
843 :param: vca_id: VCA ID
844 """
845 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
846 libjuju = await self._get_libjuju(vca_id)
847
848 # check arguments
849 if ee_id is None:
850 raise N2VCBadArgumentsException(
851 message="ee_id is mandatory", bad_args=["ee_id"]
852 )
853
854 model_name, application_name, machine_id = self._get_ee_id_components(
855 ee_id=ee_id
856 )
857 try:
858 if not scaling_in:
859 # destroy the model
860 await libjuju.destroy_model(
861 model_name=model_name,
862 total_timeout=total_timeout,
863 )
864 elif vca_type == "native_charm" and scaling_in:
865 # destroy the unit in the application
866 await libjuju.destroy_unit(
867 application_name=application_name,
868 model_name=model_name,
869 machine_id=machine_id,
870 total_timeout=total_timeout,
871 )
872 else:
873 # destroy the application
874 await libjuju.destroy_application(
875 model_name=model_name,
876 application_name=application_name,
877 total_timeout=total_timeout,
878 )
879 except Exception as e:
880 raise N2VCException(
881 message=(
882 "Error deleting execution environment {} (application {}) : {}"
883 ).format(ee_id, application_name, e)
884 )
885
886 self.log.info("Execution environment {} deleted".format(ee_id))
887
888 async def exec_primitive(
889 self,
890 ee_id: str,
891 primitive_name: str,
892 params_dict: dict,
893 db_dict: dict = None,
894 progress_timeout: float = None,
895 total_timeout: float = None,
896 vca_id: str = None,
897 vca_type: str = None,
898 ) -> str:
899 """
900 Execute a primitive in the execution environment
901
902 :param: ee_id: the one returned by create_execution_environment or
903 register_execution_environment
904 :param: primitive_name: must be one defined in the software. There is one
905 called 'config', where, for the proxy case, the 'credentials' of VM are
906 provided
907 :param: params_dict: parameters of the action
908 :param: db_dict: where to write into database when the status changes.
909 It contains a dict with
910 {collection: <str>, filter: {}, path: <str>},
911 e.g. {collection: "nsrs", filter:
912 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
913 :param: progress_timeout: Progress timeout
914 :param: total_timeout: Total timeout
915 :param: vca_id: VCA ID
916 :param: vca_type: VCA type
917 :returns str: primitive result, if ok. It raises exceptions in case of fail
918 """
919
920 self.log.info(
921 "Executing primitive: {} on ee: {}, params: {}".format(
922 primitive_name, ee_id, params_dict
923 )
924 )
925 libjuju = await self._get_libjuju(vca_id)
926
927 # check arguments
928 if ee_id is None or len(ee_id) == 0:
929 raise N2VCBadArgumentsException(
930 message="ee_id is mandatory", bad_args=["ee_id"]
931 )
932 if primitive_name is None or len(primitive_name) == 0:
933 raise N2VCBadArgumentsException(
934 message="action_name is mandatory", bad_args=["action_name"]
935 )
936 if params_dict is None:
937 params_dict = dict()
938
939 try:
940 (
941 model_name,
942 application_name,
943 machine_id,
944 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
945 # To run action on the leader unit in libjuju.execute_action function,
946 # machine_id must be set to None if vca_type is not native_charm
947 if vca_type != "native_charm":
948 machine_id = None
949 except Exception:
950 raise N2VCBadArgumentsException(
951 message="ee_id={} is not a valid execution environment id".format(
952 ee_id
953 ),
954 bad_args=["ee_id"],
955 )
956
957 if primitive_name == "config":
958 # Special case: config primitive
959 try:
960 await libjuju.configure_application(
961 model_name=model_name,
962 application_name=application_name,
963 config=params_dict,
964 )
965 actions = await libjuju.get_actions(
966 application_name=application_name,
967 model_name=model_name,
968 )
969 self.log.debug(
970 "Application {} has these actions: {}".format(
971 application_name, actions
972 )
973 )
974 if "verify-ssh-credentials" in actions:
975 # execute verify-credentials
976 num_retries = 20
977 retry_timeout = 15.0
978 for _ in range(num_retries):
979 try:
980 self.log.debug("Executing action verify-ssh-credentials...")
981 output, ok = await libjuju.execute_action(
982 model_name=model_name,
983 application_name=application_name,
984 action_name="verify-ssh-credentials",
985 db_dict=db_dict,
986 progress_timeout=progress_timeout,
987 total_timeout=total_timeout,
988 )
989
990 if ok == "failed":
991 self.log.debug(
992 "Error executing verify-ssh-credentials: {}. Retrying..."
993 )
994 await asyncio.sleep(retry_timeout)
995
996 continue
997 self.log.debug("Result: {}, output: {}".format(ok, output))
998 break
999 except asyncio.CancelledError:
1000 raise
1001 else:
1002 self.log.error(
1003 "Error executing verify-ssh-credentials after {} retries. ".format(
1004 num_retries
1005 )
1006 )
1007 else:
1008 msg = "Action verify-ssh-credentials does not exist in application {}".format(
1009 application_name
1010 )
1011 self.log.debug(msg=msg)
1012 except Exception as e:
1013 self.log.error("Error configuring juju application: {}".format(e))
1014 raise N2VCExecutionException(
1015 message="Error configuring application into ee={} : {}".format(
1016 ee_id, e
1017 ),
1018 primitive_name=primitive_name,
1019 )
1020 return "CONFIG OK"
1021 else:
1022 try:
1023 output, status = await libjuju.execute_action(
1024 model_name=model_name,
1025 application_name=application_name,
1026 action_name=primitive_name,
1027 db_dict=db_dict,
1028 machine_id=machine_id,
1029 progress_timeout=progress_timeout,
1030 total_timeout=total_timeout,
1031 **params_dict,
1032 )
1033 if status == "completed":
1034 return output
1035 else:
1036 raise Exception("status is not completed: {}".format(status))
1037 except Exception as e:
1038 self.log.error(
1039 "Error executing primitive {}: {}".format(primitive_name, e)
1040 )
1041 raise N2VCExecutionException(
1042 message="Error executing primitive {} into ee={} : {}".format(
1043 primitive_name, ee_id, e
1044 ),
1045 primitive_name=primitive_name,
1046 )
1047
1048 async def disconnect(self, vca_id: str = None):
1049 """
1050 Disconnect from VCA
1051
1052 :param: vca_id: VCA ID
1053 """
1054 self.log.info("closing juju N2VC...")
1055 libjuju = await self._get_libjuju(vca_id)
1056 try:
1057 await libjuju.disconnect()
1058 except Exception as e:
1059 raise N2VCConnectionException(
1060 message="Error disconnecting controller: {}".format(e),
1061 url=libjuju.vca_connection.data.endpoints,
1062 )
1063
1064 """
1065 ####################################################################################
1066 ################################### P R I V A T E ##################################
1067 ####################################################################################
1068 """
1069
1070 async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
1071 """
1072 Get libjuju object
1073
1074 :param: vca_id: VCA ID
1075 If None, get a libjuju object with a Connection to the default VCA
1076 Else, geta libjuju object with a Connection to the specified VCA
1077 """
1078 if not vca_id:
1079 while self.loading_libjuju.locked():
1080 await asyncio.sleep(0.1)
1081 if not self.libjuju:
1082 async with self.loading_libjuju:
1083 vca_connection = await get_connection(self._store)
1084 self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
1085 return self.libjuju
1086 else:
1087 vca_connection = await get_connection(self._store, vca_id)
1088 return Libjuju(
1089 vca_connection,
1090 loop=self.loop,
1091 log=self.log,
1092 n2vc=self,
1093 )
1094
1095 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
1096
1097 # write ee_id to database: _admin.deployed.VCA.x
1098 try:
1099 the_table = db_dict["collection"]
1100 the_filter = db_dict["filter"]
1101 the_path = db_dict["path"]
1102 if not the_path[-1] == ".":
1103 the_path = the_path + "."
1104 update_dict = {the_path + "ee_id": ee_id}
1105 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1106 self.db.set_one(
1107 table=the_table,
1108 q_filter=the_filter,
1109 update_dict=update_dict,
1110 fail_on_empty=True,
1111 )
1112 except asyncio.CancelledError:
1113 raise
1114 except Exception as e:
1115 self.log.error("Error writing ee_id to database: {}".format(e))
1116
1117 @staticmethod
1118 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
1119 """
1120 Build an execution environment id form model, application and machine
1121 :param model_name:
1122 :param application_name:
1123 :param machine_id:
1124 :return:
1125 """
1126 # id for the execution environment
1127 return "{}.{}.{}".format(model_name, application_name, machine_id)
1128
1129 @staticmethod
1130 def _get_ee_id_components(ee_id: str) -> (str, str, str):
1131 """
1132 Get model, application and machine components from an execution environment id
1133 :param ee_id:
1134 :return: model_name, application_name, machine_id
1135 """
1136
1137 return get_ee_id_components(ee_id)
1138
1139 def _get_application_name(self, namespace: str) -> str:
1140 """
1141 Build application name from namespace
1142 :param namespace:
1143 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1144 """
1145
1146 # TODO: Enforce the Juju 50-character application limit
1147
1148 # split namespace components
1149 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1150 namespace=namespace
1151 )
1152
1153 if vnf_id is None or len(vnf_id) == 0:
1154 vnf_id = ""
1155 else:
1156 # Shorten the vnf_id to its last twelve characters
1157 vnf_id = "vnf-" + vnf_id[-12:]
1158
1159 if vdu_id is None or len(vdu_id) == 0:
1160 vdu_id = ""
1161 else:
1162 # Shorten the vdu_id to its last twelve characters
1163 vdu_id = "-vdu-" + vdu_id[-12:]
1164
1165 if vdu_count is None or len(vdu_count) == 0:
1166 vdu_count = ""
1167 else:
1168 vdu_count = "-cnt-" + vdu_count
1169
1170 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1171
1172 return N2VCJujuConnector._format_app_name(application_name)
1173
1174 @staticmethod
1175 def _format_model_name(name: str) -> str:
1176 """Format the name of the model.
1177
1178 Model names may only contain lowercase letters, digits and hyphens
1179 """
1180
1181 return name.replace("_", "-").replace(" ", "-").lower()
1182
1183 @staticmethod
1184 def _format_app_name(name: str) -> str:
1185 """Format the name of the application (in order to assure valid application name).
1186
1187 Application names have restrictions (run juju deploy --help):
1188 - contains lowercase letters 'a'-'z'
1189 - contains numbers '0'-'9'
1190 - contains hyphens '-'
1191 - starts with a lowercase letter
1192 - not two or more consecutive hyphens
1193 - after a hyphen, not a group with all numbers
1194 """
1195
1196 def all_numbers(s: str) -> bool:
1197 for c in s:
1198 if not c.isdigit():
1199 return False
1200 return True
1201
1202 new_name = name.replace("_", "-")
1203 new_name = new_name.replace(" ", "-")
1204 new_name = new_name.lower()
1205 while new_name.find("--") >= 0:
1206 new_name = new_name.replace("--", "-")
1207 groups = new_name.split("-")
1208
1209 # find 'all numbers' groups and prefix them with a letter
1210 app_name = ""
1211 for i in range(len(groups)):
1212 group = groups[i]
1213 if all_numbers(group):
1214 group = "z" + group
1215 if i > 0:
1216 app_name += "-"
1217 app_name += group
1218
1219 if app_name[0].isdigit():
1220 app_name = "z" + app_name
1221
1222 return app_name
1223
1224 async def validate_vca(self, vca_id: str):
1225 """
1226 Validate a VCA by connecting/disconnecting to/from it
1227
1228 :param: vca_id: VCA ID
1229 """
1230 vca_connection = await get_connection(self._store, vca_id=vca_id)
1231 libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log, n2vc=self)
1232 controller = await libjuju.get_controller()
1233 await libjuju.disconnect_controller(controller)