Code Coverage

Cobertura Coverage Report > n2vc >

n2vc_juju_conn.py

Trend

Classes100%
 
Lines36%
   
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
n2vc_juju_conn.py
100%
1/1
36%
150/419
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
n2vc_juju_conn.py
36%
150/419
N/A

Source

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 1 import asyncio
24 1 import logging
25 1 import os
26
27 1 from n2vc.config import ModelConfig
28 1 from n2vc.exceptions import (
29     N2VCBadArgumentsException,
30     N2VCException,
31     N2VCConnectionException,
32     N2VCExecutionException,
33     # N2VCNotFound,
34     MethodNotImplemented,
35     JujuK8sProxycharmNotSupported,
36 )
37 1 from n2vc.n2vc_conn import N2VCConnector
38 1 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
39 1 from n2vc.libjuju import Libjuju
40 1 from n2vc.utils import base64_to_cacert
41
42
43 1 class N2VCJujuConnector(N2VCConnector):
44
45     """
46 ####################################################################################
47 ################################### P U B L I C ####################################
48 ####################################################################################
49     """
50
51 1     BUILT_IN_CLOUDS = ["localhost", "microk8s"]
52
53 1     def __init__(
54         self,
55         db: object,
56         fs: object,
57         log: object = None,
58         loop: object = None,
59         url: str = "127.0.0.1:17070",
60         username: str = "admin",
61         vca_config: dict = None,
62         on_update_db=None,
63     ):
64         """Initialize juju N2VC connector
65         """
66
67         # parent class constructor
68 1         N2VCConnector.__init__(
69             self,
70             db=db,
71             fs=fs,
72             log=log,
73             loop=loop,
74             url=url,
75             username=username,
76             vca_config=vca_config,
77             on_update_db=on_update_db,
78         )
79
80         # silence websocket traffic log
81 1         logging.getLogger("websockets.protocol").setLevel(logging.INFO)
82 1         logging.getLogger("juju.client.connection").setLevel(logging.WARN)
83 1         logging.getLogger("model").setLevel(logging.WARN)
84
85 1         self.log.info("Initializing N2VC juju connector...")
86
87         """
88         ##############################################################
89         # check arguments
90         ##############################################################
91         """
92
93         # juju URL
94 1         if url is None:
95 0             raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
96 1         url_parts = url.split(":")
97 1         if len(url_parts) != 2:
98 0             raise N2VCBadArgumentsException(
99                 "Argument url: bad format (localhost:port) -> {}".format(url), ["url"]
100             )
101 1         self.hostname = url_parts[0]
102 1         try:
103 1             self.port = int(url_parts[1])
104 0         except ValueError:
105 0             raise N2VCBadArgumentsException(
106                 "url port must be a number -> {}".format(url), ["url"]
107             )
108
109         # juju USERNAME
110 1         if username is None:
111 0             raise N2VCBadArgumentsException(
112                 "Argument username is mandatory", ["username"]
113             )
114
115         # juju CONFIGURATION
116 1         if vca_config is None:
117 0             raise N2VCBadArgumentsException(
118                 "Argument vca_config is mandatory", ["vca_config"]
119             )
120
121 1         if "secret" in vca_config:
122 1             self.secret = vca_config["secret"]
123         else:
124 0             raise N2VCBadArgumentsException(
125                 "Argument vca_config.secret is mandatory", ["vca_config.secret"]
126             )
127
128         # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
129         # if exists, it will be written in lcm container: _create_juju_public_key()
130 1         if "public_key" in vca_config:
131 0             self.public_key = vca_config["public_key"]
132         else:
133 1             self.public_key = None
134
135         # TODO: Verify ca_cert is valid before using. VCA will crash
136         # if the ca_cert isn't formatted correctly.
137
138 1         self.ca_cert = vca_config.get("ca_cert")
139 1         if self.ca_cert:
140 0             self.ca_cert = base64_to_cacert(vca_config["ca_cert"])
141
142 1         if "api_proxy" in vca_config and vca_config["api_proxy"] != "":
143 1             self.api_proxy = vca_config["api_proxy"]
144 1             self.log.debug(
145                 "api_proxy for native charms configured: {}".format(self.api_proxy)
146             )
147         else:
148 0             self.warning(
149                 "api_proxy is not configured"
150             )
151 0             self.api_proxy = None
152
153 1         model_config = ModelConfig(vca_config)
154
155 1         self.cloud = vca_config.get('cloud')
156 1         self.k8s_cloud = None
157 1         if "k8s_cloud" in vca_config:
158 1             self.k8s_cloud = vca_config.get("k8s_cloud")
159 1         self.log.debug('Arguments have been checked')
160
161         # juju data
162 1         self.controller = None  # it will be filled when connect to juju
163 1         self.juju_models = {}  # model objects for every model_name
164 1         self.juju_observers = {}  # model observers for every model_name
165 1         self._connecting = (
166             False  # while connecting to juju (to avoid duplicate connections)
167         )
168 1         self._authenticated = (
169             False  # it will be True when juju connection be stablished
170         )
171 1         self._creating_model = False  # True during model creation
172 1         self.libjuju = Libjuju(
173             endpoint=self.url,
174             api_proxy=self.api_proxy,
175             username=self.username,
176             password=self.secret,
177             cacert=self.ca_cert,
178             loop=self.loop,
179             log=self.log,
180             db=self.db,
181             n2vc=self,
182             model_config=model_config,
183         )
184
185         # create juju pub key file in lcm container at
186         # ./local/share/juju/ssh/juju_id_rsa.pub
187 1         self._create_juju_public_key()
188
189 1         self.log.info("N2VC juju connector initialized")
190
191 1     async def get_status(self, namespace: str, yaml_format: bool = True):
192
193         # self.log.info('Getting NS status. namespace: {}'.format(namespace))
194
195 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
196             namespace=namespace
197         )
198         # model name is ns_id
199 0         model_name = ns_id
200 0         if model_name is None:
201 0             msg = "Namespace {} not valid".format(namespace)
202 0             self.log.error(msg)
203 0             raise N2VCBadArgumentsException(msg, ["namespace"])
204
205 0         status = {}
206 0         models = await self.libjuju.list_models(contains=ns_id)
207
208 0         for m in models:
209 0             status[m] = await self.libjuju.get_model_status(m)
210 0         if yaml_format:
211 0             return obj_to_yaml(status)
212         else:
213 0             return obj_to_dict(status)
214
215 1     async def update_vca_status(self, vcastatus: dict):
216         """
217         Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
218         :param vcastatus: dict containing vcaStatus
219         :return: None
220         """
221 1         try:
222 1             for model_name in vcastatus:
223                 # Adding executed actions
224 1                 vcastatus[model_name]["executedActions"] = \
225                     await self.libjuju.get_executed_actions(model_name)
226 1                 for application in vcastatus[model_name]["applications"]:
227                     # Adding application actions
228 1                     vcastatus[model_name]["applications"][application]["actions"] = \
229                         await self.libjuju.get_actions(application, model_name)
230                     # Adding application configs
231 1                     vcastatus[model_name]["applications"][application]["configs"] = \
232                         await self.libjuju.get_application_configs(model_name, application)
233 1         except Exception as e:
234 1             self.log.debug("Error in updating vca status: {}".format(str(e)))
235
236 1     async def create_execution_environment(
237         self,
238         namespace: str,
239         db_dict: dict,
240         reuse_ee_id: str = None,
241         progress_timeout: float = None,
242         total_timeout: float = None,
243         cloud_name: str = None,
244         credential_name: str = None,
245     ) -> (str, dict):
246
247 0         self.log.info(
248             "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
249                 namespace, reuse_ee_id
250             )
251         )
252
253 0         machine_id = None
254 0         if reuse_ee_id:
255 0             model_name, application_name, machine_id = self._get_ee_id_components(
256                 ee_id=reuse_ee_id
257             )
258         else:
259 0             (
260                 _nsi_id,
261                 ns_id,
262                 _vnf_id,
263                 _vdu_id,
264                 _vdu_count,
265             ) = self._get_namespace_components(namespace=namespace)
266             # model name is ns_id
267 0             model_name = ns_id
268             # application name
269 0             application_name = self._get_application_name(namespace=namespace)
270
271 0         self.log.debug(
272             "model name: {}, application name:  {}, machine_id: {}".format(
273                 model_name, application_name, machine_id
274             )
275         )
276
277         # create or reuse a new juju machine
278 0         try:
279 0             if not await self.libjuju.model_exists(model_name):
280 0                 cloud = cloud_name or self.cloud
281 0                 credential = credential_name or cloud_name if cloud_name else self.cloud
282 0                 await self.libjuju.add_model(
283                     model_name,
284                     cloud_name=cloud,
285                     credential_name=credential
286                 )
287 0             machine, new = await self.libjuju.create_machine(
288                 model_name=model_name,
289                 machine_id=machine_id,
290                 db_dict=db_dict,
291                 progress_timeout=progress_timeout,
292                 total_timeout=total_timeout,
293             )
294             # id for the execution environment
295 0             ee_id = N2VCJujuConnector._build_ee_id(
296                 model_name=model_name,
297                 application_name=application_name,
298                 machine_id=str(machine.entity_id),
299             )
300 0             self.log.debug("ee_id: {}".format(ee_id))
301
302 0             if new:
303                 # write ee_id in database
304 0                 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
305
306 0         except Exception as e:
307 0             message = "Error creating machine on juju: {}".format(e)
308 0             self.log.error(message)
309 0             raise N2VCException(message=message)
310
311         # new machine credentials
312 0         credentials = {
313             "hostname": machine.dns_name,
314         }
315
316 0         self.log.info(
317             "Execution environment created. ee_id: {}, credentials: {}".format(
318                 ee_id, credentials
319             )
320         )
321
322 0         return ee_id, credentials
323
324 1     async def register_execution_environment(
325         self,
326         namespace: str,
327         credentials: dict,
328         db_dict: dict,
329         progress_timeout: float = None,
330         total_timeout: float = None,
331         cloud_name: str = None,
332         credential_name: str = None,
333     ) -> str:
334
335 0         self.log.info(
336             "Registering execution environment. namespace={}, credentials={}".format(
337                 namespace, credentials
338             )
339         )
340
341 0         if credentials is None:
342 0             raise N2VCBadArgumentsException(
343                 message="credentials are mandatory", bad_args=["credentials"]
344             )
345 0         if credentials.get("hostname"):
346 0             hostname = credentials["hostname"]
347         else:
348 0             raise N2VCBadArgumentsException(
349                 message="hostname is mandatory", bad_args=["credentials.hostname"]
350             )
351 0         if credentials.get("username"):
352 0             username = credentials["username"]
353         else:
354 0             raise N2VCBadArgumentsException(
355                 message="username is mandatory", bad_args=["credentials.username"]
356             )
357 0         if "private_key_path" in credentials:
358 0             private_key_path = credentials["private_key_path"]
359         else:
360             # if not passed as argument, use generated private key path
361 0             private_key_path = self.private_key_path
362
363 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
364             namespace=namespace
365         )
366
367         # model name
368 0         model_name = ns_id
369         # application name
370 0         application_name = self._get_application_name(namespace=namespace)
371
372         # register machine on juju
373 0         try:
374 0             if not await self.libjuju.model_exists(model_name):
375 0                 cloud = cloud_name or self.cloud
376 0                 credential = credential_name or cloud_name if cloud_name else self.cloud
377 0                 await self.libjuju.add_model(
378                     model_name,
379                     cloud_name=cloud,
380                     credential_name=credential
381                 )
382 0             machine_id = await self.libjuju.provision_machine(
383                 model_name=model_name,
384                 hostname=hostname,
385                 username=username,
386                 private_key_path=private_key_path,
387                 db_dict=db_dict,
388                 progress_timeout=progress_timeout,
389                 total_timeout=total_timeout,
390             )
391 0         except Exception as e:
392 0             self.log.error("Error registering machine: {}".format(e))
393 0             raise N2VCException(
394                 message="Error registering machine on juju: {}".format(e)
395             )
396
397 0         self.log.info("Machine registered: {}".format(machine_id))
398
399         # id for the execution environment
400 0         ee_id = N2VCJujuConnector._build_ee_id(
401             model_name=model_name,
402             application_name=application_name,
403             machine_id=str(machine_id),
404         )
405
406 0         self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
407
408 0         return ee_id
409
410 1     async def install_configuration_sw(
411         self,
412         ee_id: str,
413         artifact_path: str,
414         db_dict: dict,
415         progress_timeout: float = None,
416         total_timeout: float = None,
417         config: dict = None,
418         num_units: int = 1,
419     ):
420
421 0         self.log.info(
422             (
423                 "Installing configuration sw on ee_id: {}, "
424                 "artifact path: {}, db_dict: {}"
425             ).format(ee_id, artifact_path, db_dict)
426         )
427
428         # check arguments
429 0         if ee_id is None or len(ee_id) == 0:
430 0             raise N2VCBadArgumentsException(
431                 message="ee_id is mandatory", bad_args=["ee_id"]
432             )
433 0         if artifact_path is None or len(artifact_path) == 0:
434 0             raise N2VCBadArgumentsException(
435                 message="artifact_path is mandatory", bad_args=["artifact_path"]
436             )
437 0         if db_dict is None:
438 0             raise N2VCBadArgumentsException(
439                 message="db_dict is mandatory", bad_args=["db_dict"]
440             )
441
442 0         try:
443 0             (
444                 model_name,
445                 application_name,
446                 machine_id,
447             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
448 0             self.log.debug(
449                 "model: {}, application: {}, machine: {}".format(
450                     model_name, application_name, machine_id
451                 )
452             )
453 0         except Exception:
454 0             raise N2VCBadArgumentsException(
455                 message="ee_id={} is not a valid execution environment id".format(
456                     ee_id
457                 ),
458                 bad_args=["ee_id"],
459             )
460
461         # remove // in charm path
462 0         while artifact_path.find("//") >= 0:
463 0             artifact_path = artifact_path.replace("//", "/")
464
465         # check charm path
466 0         if not self.fs.file_exists(artifact_path, mode="dir"):
467 0             msg = "artifact path does not exist: {}".format(artifact_path)
468 0             raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
469
470 0         if artifact_path.startswith("/"):
471 0             full_path = self.fs.path + artifact_path
472         else:
473 0             full_path = self.fs.path + "/" + artifact_path
474
475 0         try:
476 0             await self.libjuju.deploy_charm(
477                 model_name=model_name,
478                 application_name=application_name,
479                 path=full_path,
480                 machine_id=machine_id,
481                 db_dict=db_dict,
482                 progress_timeout=progress_timeout,
483                 total_timeout=total_timeout,
484                 config=config,
485                 num_units=num_units,
486             )
487 0         except Exception as e:
488 0             raise N2VCException(
489                 message="Error desploying charm into ee={} : {}".format(ee_id, e)
490             )
491
492 0         self.log.info("Configuration sw installed")
493
494 1     async def install_k8s_proxy_charm(
495         self,
496         charm_name: str,
497         namespace: str,
498         artifact_path: str,
499         db_dict: dict,
500         progress_timeout: float = None,
501         total_timeout: float = None,
502         config: dict = None,
503         cloud_name: str = None,
504         credential_name: str = None,
505     ) -> str:
506         """
507         Install a k8s proxy charm
508
509         :param charm_name: Name of the charm being deployed
510         :param namespace: collection of all the uuids related to the charm.
511         :param str artifact_path: where to locate the artifacts (parent folder) using
512             the self.fs
513             the final artifact path will be a combination of this artifact_path and
514             additional string from the config_dict (e.g. charm name)
515         :param dict db_dict: where to write into database when the status changes.
516                         It contains a dict with
517                             {collection: <str>, filter: {},  path: <str>},
518                             e.g. {collection: "nsrs", filter:
519                                 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
520         :param float progress_timeout:
521         :param float total_timeout:
522         :param config: Dictionary with additional configuration
523         :param cloud_name: Cloud Name in which the charms will be deployed
524         :param credential_name: Credential Name to use in the cloud_name.
525                                 If not set, cloud_name will be used as credential_name
526
527         :returns ee_id: execution environment id.
528         """
529 1         self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
530                       .format(charm_name, artifact_path, db_dict))
531
532 1         if not self.k8s_cloud:
533 1             raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
534
535 1         if artifact_path is None or len(artifact_path) == 0:
536 1             raise N2VCBadArgumentsException(
537                 message="artifact_path is mandatory", bad_args=["artifact_path"]
538             )
539 1         if db_dict is None:
540 1             raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
541
542         # remove // in charm path
543 1         while artifact_path.find('//') >= 0:
544 1             artifact_path = artifact_path.replace('//', '/')
545
546         # check charm path
547 1         if not self.fs.file_exists(artifact_path, mode="dir"):
548 1             msg = 'artifact path does not exist: {}'.format(artifact_path)
549 1             raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
550
551 1         if artifact_path.startswith('/'):
552 1             full_path = self.fs.path + artifact_path
553         else:
554 1             full_path = self.fs.path + '/' + artifact_path
555
556 1         _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
557 1         model_name = '{}-k8s'.format(ns_id)
558 1         if not await self.libjuju.model_exists(model_name):
559 1             cloud = cloud_name or self.k8s_cloud
560 1             credential = credential_name or cloud_name if cloud_name else self.k8s_cloud
561 1             await self.libjuju.add_model(
562                 model_name,
563                 cloud_name=cloud,
564                 credential_name=credential
565             )
566 1         application_name = self._get_application_name(namespace)
567
568 1         try:
569 1             await self.libjuju.deploy_charm(
570                 model_name=model_name,
571                 application_name=application_name,
572                 path=full_path,
573                 machine_id=None,
574                 db_dict=db_dict,
575                 progress_timeout=progress_timeout,
576                 total_timeout=total_timeout,
577                 config=config
578             )
579 1         except Exception as e:
580 1             raise N2VCException(message='Error deploying charm: {}'.format(e))
581
582 1         self.log.info('K8s proxy charm installed')
583 1         ee_id = N2VCJujuConnector._build_ee_id(
584             model_name=model_name,
585             application_name=application_name,
586             machine_id="k8s",
587         )
588
589 1         self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
590
591 1         return ee_id
592
593 1     async def get_ee_ssh_public__key(
594         self,
595         ee_id: str,
596         db_dict: dict,
597         progress_timeout: float = None,
598         total_timeout: float = None,
599     ) -> str:
600
601 0         self.log.info(
602             (
603                 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
604             ).format(ee_id, db_dict)
605         )
606
607         # check arguments
608 0         if ee_id is None or len(ee_id) == 0:
609 0             raise N2VCBadArgumentsException(
610                 message="ee_id is mandatory", bad_args=["ee_id"]
611             )
612 0         if db_dict is None:
613 0             raise N2VCBadArgumentsException(
614                 message="db_dict is mandatory", bad_args=["db_dict"]
615             )
616
617 0         try:
618 0             (
619                 model_name,
620                 application_name,
621                 machine_id,
622             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
623 0             self.log.debug(
624                 "model: {}, application: {}, machine: {}".format(
625                     model_name, application_name, machine_id
626                 )
627             )
628 0         except Exception:
629 0             raise N2VCBadArgumentsException(
630                 message="ee_id={} is not a valid execution environment id".format(
631                     ee_id
632                 ),
633                 bad_args=["ee_id"],
634             )
635
636         # try to execute ssh layer primitives (if exist):
637         #       generate-ssh-key
638         #       get-ssh-public-key
639
640 0         output = None
641
642 0         application_name = N2VCJujuConnector._format_app_name(application_name)
643
644         # execute action: generate-ssh-key
645 0         try:
646 0             output, _status = await self.libjuju.execute_action(
647                 model_name=model_name,
648                 application_name=application_name,
649                 action_name="generate-ssh-key",
650                 db_dict=db_dict,
651                 progress_timeout=progress_timeout,
652                 total_timeout=total_timeout,
653             )
654 0         except Exception as e:
655 0             self.log.info(
656                 "Skipping exception while executing action generate-ssh-key: {}".format(
657                     e
658                 )
659             )
660
661         # execute action: get-ssh-public-key
662 0         try:
663 0             output, _status = await self.libjuju.execute_action(
664                 model_name=model_name,
665                 application_name=application_name,
666                 action_name="get-ssh-public-key",
667                 db_dict=db_dict,
668                 progress_timeout=progress_timeout,
669                 total_timeout=total_timeout,
670             )
671 0         except Exception as e:
672 0             msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
673 0             self.log.info(msg)
674 0             raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
675
676         # return public key if exists
677 0         return output["pubkey"] if "pubkey" in output else output
678
679 1     async def get_metrics(self, model_name: str, application_name: str) -> dict:
680 1         return await self.libjuju.get_metrics(model_name, application_name)
681
682 1     async def add_relation(
683         self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
684     ):
685
686 0         self.log.debug(
687             "adding new relation between {} and {}, endpoints: {}, {}".format(
688                 ee_id_1, ee_id_2, endpoint_1, endpoint_2
689             )
690         )
691
692         # check arguments
693 0         if not ee_id_1:
694 0             message = "EE 1 is mandatory"
695 0             self.log.error(message)
696 0             raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
697 0         if not ee_id_2:
698 0             message = "EE 2 is mandatory"
699 0             self.log.error(message)
700 0             raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
701 0         if not endpoint_1:
702 0             message = "endpoint 1 is mandatory"
703 0             self.log.error(message)
704 0             raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
705 0         if not endpoint_2:
706 0             message = "endpoint 2 is mandatory"
707 0             self.log.error(message)
708 0             raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
709
710         # get the model, the applications and the machines from the ee_id's
711 0         model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
712 0         model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
713
714         # model must be the same
715 0         if model_1 != model_2:
716 0             message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
717 0             self.log.error(message)
718 0             raise N2VCBadArgumentsException(
719                 message=message, bad_args=["ee_id_1", "ee_id_2"]
720             )
721
722         # add juju relations between two applications
723 0         try:
724 0             await self.libjuju.add_relation(
725                 model_name=model_1,
726                 endpoint_1="{}:{}".format(app_1, endpoint_1),
727                 endpoint_2="{}:{}".format(app_2, endpoint_2),
728             )
729 0         except Exception as e:
730 0             message = "Error adding relation between {} and {}: {}".format(
731                 ee_id_1, ee_id_2, e
732             )
733 0             self.log.error(message)
734 0             raise N2VCException(message=message)
735
736 1     async def remove_relation(self):
737         # TODO
738 0         self.log.info("Method not implemented yet")
739 0         raise MethodNotImplemented()
740
741 1     async def deregister_execution_environments(self):
742 0         self.log.info("Method not implemented yet")
743 0         raise MethodNotImplemented()
744
745 1     async def delete_namespace(
746         self, namespace: str, db_dict: dict = None, total_timeout: float = None
747     ):
748 0         self.log.info("Deleting namespace={}".format(namespace))
749
750         # check arguments
751 0         if namespace is None:
752 0             raise N2VCBadArgumentsException(
753                 message="namespace is mandatory", bad_args=["namespace"]
754             )
755
756 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
757             namespace=namespace
758         )
759 0         if ns_id is not None:
760 0             try:
761 0                 models = await self.libjuju.list_models(contains=ns_id)
762 0                 for model in models:
763 0                     await self.libjuju.destroy_model(
764                         model_name=model, total_timeout=total_timeout
765                     )
766 0             except Exception as e:
767 0                 raise N2VCException(
768                     message="Error deleting namespace {} : {}".format(namespace, e)
769                 )
770         else:
771 0             raise N2VCBadArgumentsException(
772                 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
773             )
774
775 0         self.log.info("Namespace {} deleted".format(namespace))
776
777 1     async def delete_execution_environment(
778         self, ee_id: str, db_dict: dict = None, total_timeout: float = None,
779             scaling_in: bool = False
780     ):
781 0         self.log.info("Deleting execution environment ee_id={}".format(ee_id))
782
783         # check arguments
784 0         if ee_id is None:
785 0             raise N2VCBadArgumentsException(
786                 message="ee_id is mandatory", bad_args=["ee_id"]
787             )
788
789 0         model_name, application_name, _machine_id = self._get_ee_id_components(
790             ee_id=ee_id
791         )
792 0         try:
793 0             if not scaling_in:
794                 # destroy the model
795                 # TODO: should this be removed?
796 0                 await self.libjuju.destroy_model(
797                     model_name=model_name, total_timeout=total_timeout
798                 )
799             else:
800                 # get juju model and observer
801 0                 controller = await self.libjuju.get_controller()
802 0                 model = await self.libjuju.get_model(controller, model_name)
803                 # destroy the application
804 0                 await self.libjuju.destroy_application(
805                     model=model, application_name=application_name)
806 0         except Exception as e:
807 0             raise N2VCException(
808                 message=(
809                     "Error deleting execution environment {} (application {}) : {}"
810                 ).format(ee_id, application_name, e)
811             )
812
813 0         self.log.info("Execution environment {} deleted".format(ee_id))
814
815 1     async def exec_primitive(
816         self,
817         ee_id: str,
818         primitive_name: str,
819         params_dict: dict,
820         db_dict: dict = None,
821         progress_timeout: float = None,
822         total_timeout: float = None,
823     ) -> str:
824
825 0         self.log.info(
826             "Executing primitive: {} on ee: {}, params: {}".format(
827                 primitive_name, ee_id, params_dict
828             )
829         )
830
831         # check arguments
832 0         if ee_id is None or len(ee_id) == 0:
833 0             raise N2VCBadArgumentsException(
834                 message="ee_id is mandatory", bad_args=["ee_id"]
835             )
836 0         if primitive_name is None or len(primitive_name) == 0:
837 0             raise N2VCBadArgumentsException(
838                 message="action_name is mandatory", bad_args=["action_name"]
839             )
840 0         if params_dict is None:
841 0             params_dict = dict()
842
843 0         try:
844 0             (
845                 model_name,
846                 application_name,
847                 _machine_id,
848             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
849 0         except Exception:
850 0             raise N2VCBadArgumentsException(
851                 message="ee_id={} is not a valid execution environment id".format(
852                     ee_id
853                 ),
854                 bad_args=["ee_id"],
855             )
856
857 0         if primitive_name == "config":
858             # Special case: config primitive
859 0             try:
860 0                 await self.libjuju.configure_application(
861                     model_name=model_name,
862                     application_name=application_name,
863                     config=params_dict,
864                 )
865 0                 actions = await self.libjuju.get_actions(
866                     application_name=application_name, model_name=model_name,
867                 )
868 0                 self.log.debug(
869                     "Application {} has these actions: {}".format(
870                         application_name, actions
871                     )
872                 )
873 0                 if "verify-ssh-credentials" in actions:
874                     # execute verify-credentials
875 0                     num_retries = 20
876 0                     retry_timeout = 15.0
877 0                     for _ in range(num_retries):
878 0                         try:
879 0                             self.log.debug("Executing action verify-ssh-credentials...")
880 0                             output, ok = await self.libjuju.execute_action(
881                                 model_name=model_name,
882                                 application_name=application_name,
883                                 action_name="verify-ssh-credentials",
884                                 db_dict=db_dict,
885                                 progress_timeout=progress_timeout,
886                                 total_timeout=total_timeout,
887                             )
888
889 0                             if ok == "failed":
890 0                                 self.log.debug(
891                                     "Error executing verify-ssh-credentials: {}. Retrying..."
892                                 )
893 0                                 await asyncio.sleep(retry_timeout)
894
895 0                                 continue
896 0                             self.log.debug("Result: {}, output: {}".format(ok, output))
897 0                             break
898 0                         except asyncio.CancelledError:
899 0                             raise
900                     else:
901 0                         self.log.error(
902                             "Error executing verify-ssh-credentials after {} retries. ".format(
903                                 num_retries
904                             )
905                         )
906                 else:
907 0                     msg = "Action verify-ssh-credentials does not exist in application {}".format(
908                         application_name
909                     )
910 0                     self.log.debug(msg=msg)
911 0             except Exception as e:
912 0                 self.log.error("Error configuring juju application: {}".format(e))
913 0                 raise N2VCExecutionException(
914                     message="Error configuring application into ee={} : {}".format(
915                         ee_id, e
916                     ),
917                     primitive_name=primitive_name,
918                 )
919 0             return "CONFIG OK"
920         else:
921 0             try:
922 0                 output, status = await self.libjuju.execute_action(
923                     model_name=model_name,
924                     application_name=application_name,
925                     action_name=primitive_name,
926                     db_dict=db_dict,
927                     progress_timeout=progress_timeout,
928                     total_timeout=total_timeout,
929                     **params_dict
930                 )
931 0                 if status == "completed":
932 0                     return output
933                 else:
934 0                     raise Exception("status is not completed: {}".format(status))
935 0             except Exception as e:
936 0                 self.log.error(
937                     "Error executing primitive {}: {}".format(primitive_name, e)
938                 )
939 0                 raise N2VCExecutionException(
940                     message="Error executing primitive {} into ee={} : {}".format(
941                         primitive_name, ee_id, e
942                     ),
943                     primitive_name=primitive_name,
944                 )
945
946 1     async def disconnect(self):
947 0         self.log.info("closing juju N2VC...")
948 0         try:
949 0             await self.libjuju.disconnect()
950 0         except Exception as e:
951 0             raise N2VCConnectionException(
952                 message="Error disconnecting controller: {}".format(e), url=self.url
953             )
954
955     """
956 ####################################################################################
957 ################################### P R I V A T E ##################################
958 ####################################################################################
959     """
960
961 1     def _write_ee_id_db(self, db_dict: dict, ee_id: str):
962
963         # write ee_id to database: _admin.deployed.VCA.x
964 1         try:
965 1             the_table = db_dict["collection"]
966 0             the_filter = db_dict["filter"]
967 0             the_path = db_dict["path"]
968 0             if not the_path[-1] == ".":
969 0                 the_path = the_path + "."
970 0             update_dict = {the_path + "ee_id": ee_id}
971             # self.log.debug('Writing ee_id to database: {}'.format(the_path))
972 0             self.db.set_one(
973                 table=the_table,
974                 q_filter=the_filter,
975                 update_dict=update_dict,
976                 fail_on_empty=True,
977             )
978 1         except asyncio.CancelledError:
979 0             raise
980 1         except Exception as e:
981 1             self.log.error("Error writing ee_id to database: {}".format(e))
982
983 1     @staticmethod
984 1     def _build_ee_id(model_name: str, application_name: str, machine_id: str):
985         """
986         Build an execution environment id form model, application and machine
987         :param model_name:
988         :param application_name:
989         :param machine_id:
990         :return:
991         """
992         # id for the execution environment
993 1         return "{}.{}.{}".format(model_name, application_name, machine_id)
994
995 1     @staticmethod
996 1     def _get_ee_id_components(ee_id: str) -> (str, str, str):
997         """
998         Get model, application and machine components from an execution environment id
999         :param ee_id:
1000         :return: model_name, application_name, machine_id
1001         """
1002
1003 0         if ee_id is None:
1004 0             return None, None, None
1005
1006         # split components of id
1007 0         parts = ee_id.split(".")
1008 0         model_name = parts[0]
1009 0         application_name = parts[1]
1010 0         machine_id = parts[2]
1011 0         return model_name, application_name, machine_id
1012
1013 1     def _get_application_name(self, namespace: str) -> str:
1014         """
1015         Build application name from namespace
1016         :param namespace:
1017         :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1018         """
1019
1020         # TODO: Enforce the Juju 50-character application limit
1021
1022         # split namespace components
1023 1         _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1024             namespace=namespace
1025         )
1026
1027 1         if vnf_id is None or len(vnf_id) == 0:
1028 0             vnf_id = ""
1029         else:
1030             # Shorten the vnf_id to its last twelve characters
1031 1             vnf_id = "vnf-" + vnf_id[-12:]
1032
1033 1         if vdu_id is None or len(vdu_id) == 0:
1034 0             vdu_id = ""
1035         else:
1036             # Shorten the vdu_id to its last twelve characters
1037 1             vdu_id = "-vdu-" + vdu_id[-12:]
1038
1039 1         if vdu_count is None or len(vdu_count) == 0:
1040 1             vdu_count = ""
1041         else:
1042 0             vdu_count = "-cnt-" + vdu_count
1043
1044 1         application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1045
1046 1         return N2VCJujuConnector._format_app_name(application_name)
1047
1048 1     def _create_juju_public_key(self):
1049         """Recreate the Juju public key on lcm container, if needed
1050         Certain libjuju commands expect to be run from the same machine as Juju
1051          is bootstrapped to. This method will write the public key to disk in
1052          that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1053         """
1054
1055         # Make sure that we have a public key before writing to disk
1056 1         if self.public_key is None or len(self.public_key) == 0:
1057 1             if "OSMLCM_VCA_PUBKEY" in os.environ:
1058 0                 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1059 0                 if len(self.public_key) == 0:
1060 0                     return
1061             else:
1062 1                 return
1063
1064 0         pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1065 0         file_path = "{}/juju_id_rsa.pub".format(pk_path)
1066 0         self.log.debug(
1067             "writing juju public key to file:\n{}\npublic key: {}".format(
1068                 file_path, self.public_key
1069             )
1070         )
1071 0         if not os.path.exists(pk_path):
1072             # create path and write file
1073 0             os.makedirs(pk_path)
1074 0             with open(file_path, "w") as f:
1075 0                 self.log.debug("Creating juju public key file: {}".format(file_path))
1076 0                 f.write(self.public_key)
1077         else:
1078 0             self.log.debug("juju public key file already exists: {}".format(file_path))
1079
1080 1     @staticmethod
1081 1     def _format_model_name(name: str) -> str:
1082         """Format the name of the model.
1083
1084         Model names may only contain lowercase letters, digits and hyphens
1085         """
1086
1087 0         return name.replace("_", "-").replace(" ", "-").lower()
1088
1089 1     @staticmethod
1090 1     def _format_app_name(name: str) -> str:
1091         """Format the name of the application (in order to assure valid application name).
1092
1093         Application names have restrictions (run juju deploy --help):
1094             - contains lowercase letters 'a'-'z'
1095             - contains numbers '0'-'9'
1096             - contains hyphens '-'
1097             - starts with a lowercase letter
1098             - not two or more consecutive hyphens
1099             - after a hyphen, not a group with all numbers
1100         """
1101
1102 1         def all_numbers(s: str) -> bool:
1103 1             for c in s:
1104 1                 if not c.isdigit():
1105 1                     return False
1106 0             return True
1107
1108 1         new_name = name.replace("_", "-")
1109 1         new_name = new_name.replace(" ", "-")
1110 1         new_name = new_name.lower()
1111 1         while new_name.find("--") >= 0:
1112 0             new_name = new_name.replace("--", "-")
1113 1         groups = new_name.split("-")
1114
1115         # find 'all numbers' groups and prefix them with a letter
1116 1         app_name = ""
1117 1         for i in range(len(groups)):
1118 1             group = groups[i]
1119 1             if all_numbers(group):
1120 0                 group = "z" + group
1121 1             if i > 0:
1122 1                 app_name += "-"
1123 1             app_name += group
1124
1125 1         if app_name[0].isdigit():
1126 0             app_name = "z" + app_name
1127
1128 1         return app_name