Code Coverage

Cobertura Coverage Report > n2vc >

n2vc_juju_conn.py

Trend

Classes0%
 
Lines0%
 
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
n2vc_juju_conn.py
0%
0/1
0%
0/690
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
n2vc_juju_conn.py
0%
0/690
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 0 import asyncio
24 0 import base64
25 0 import binascii
26 0 import logging
27 0 import os
28 0 import re
29 0 import time
30
31 0 from juju.action import Action
32 0 from juju.application import Application
33 0 from juju.client import client
34 0 from juju.controller import Controller
35 0 from juju.errors import JujuAPIError
36 0 from juju.machine import Machine
37 0 from juju.model import Model
38 0 from n2vc.exceptions import (
39     N2VCBadArgumentsException,
40     N2VCException,
41     N2VCConnectionException,
42     N2VCExecutionException,
43     N2VCInvalidCertificate,
44     N2VCNotFound,
45     MethodNotImplemented,
46 )
47 0 from n2vc.juju_observer import JujuModelObserver
48 0 from n2vc.n2vc_conn import N2VCConnector
49 0 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
50 0 from n2vc.provisioner import AsyncSSHProvisioner
51
52
53 0 class N2VCJujuConnector(N2VCConnector):
54
55     """
56     ####################################################################################
57     ################################### P U B L I C ####################################
58     ####################################################################################
59     """
60
61 0     BUILT_IN_CLOUDS = ["localhost", "microk8s"]
62
63 0     def __init__(
64         self,
65         db: object,
66         fs: object,
67         log: object = None,
68         loop: object = None,
69         url: str = "127.0.0.1:17070",
70         username: str = "admin",
71         vca_config: dict = None,
72         on_update_db=None,
73     ):
74         """Initialize juju N2VC connector
75         """
76
77         # parent class constructor
78 0         N2VCConnector.__init__(
79             self,
80             db=db,
81             fs=fs,
82             log=log,
83             loop=loop,
84             url=url,
85             username=username,
86             vca_config=vca_config,
87             on_update_db=on_update_db,
88         )
89
90         # silence websocket traffic log
91 0         logging.getLogger("websockets.protocol").setLevel(logging.INFO)
92 0         logging.getLogger("juju.client.connection").setLevel(logging.WARN)
93 0         logging.getLogger("model").setLevel(logging.WARN)
94
95 0         self.log.info("Initializing N2VC juju connector...")
96
97         """
98         ##############################################################
99         # check arguments
100         ##############################################################
101         """
102
103         # juju URL
104 0         if url is None:
105 0             raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
106 0         url_parts = url.split(":")
107 0         if len(url_parts) != 2:
108 0             raise N2VCBadArgumentsException(
109                 "Argument url: bad format (localhost:port) -> {}".format(url), ["url"]
110             )
111 0         self.hostname = url_parts[0]
112 0         try:
113 0             self.port = int(url_parts[1])
114 0         except ValueError:
115 0             raise N2VCBadArgumentsException(
116                 "url port must be a number -> {}".format(url), ["url"]
117             )
118
119         # juju USERNAME
120 0         if username is None:
121 0             raise N2VCBadArgumentsException(
122                 "Argument username is mandatory", ["username"]
123             )
124
125         # juju CONFIGURATION
126 0         if vca_config is None:
127 0             raise N2VCBadArgumentsException(
128                 "Argument vca_config is mandatory", ["vca_config"]
129             )
130
131 0         if "secret" in vca_config:
132 0             self.secret = vca_config["secret"]
133         else:
134 0             raise N2VCBadArgumentsException(
135                 "Argument vca_config.secret is mandatory", ["vca_config.secret"]
136             )
137
138         # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
139         # if exists, it will be written in lcm container: _create_juju_public_key()
140 0         if "public_key" in vca_config:
141 0             self.public_key = vca_config["public_key"]
142         else:
143 0             self.public_key = None
144
145         # TODO: Verify ca_cert is valid before using. VCA will crash
146         # if the ca_cert isn't formatted correctly.
147 0         def base64_to_cacert(b64string):
148             """Convert the base64-encoded string containing the VCA CACERT.
149
150             The input string....
151
152             """
153 0             try:
154 0                 cacert = base64.b64decode(b64string).decode("utf-8")
155
156 0                 cacert = re.sub(r"\\n", r"\n", cacert,)
157 0             except binascii.Error as e:
158 0                 self.log.debug("Caught binascii.Error: {}".format(e))
159 0                 raise N2VCInvalidCertificate(message="Invalid CA Certificate")
160
161 0             return cacert
162
163 0         self.ca_cert = vca_config.get("ca_cert")
164 0         if self.ca_cert:
165 0             self.ca_cert = base64_to_cacert(vca_config["ca_cert"])
166
167 0         if "api_proxy" in vca_config:
168 0             self.api_proxy = vca_config["api_proxy"]
169 0             self.log.debug(
170                 "api_proxy for native charms configured: {}".format(self.api_proxy)
171             )
172         else:
173 0             self.warning(
174                 "api_proxy is not configured. Support for native charms is disabled"
175             )
176
177 0         if "enable_os_upgrade" in vca_config:
178 0             self.enable_os_upgrade = vca_config["enable_os_upgrade"]
179         else:
180 0             self.enable_os_upgrade = True
181
182 0         if "apt_mirror" in vca_config:
183 0             self.apt_mirror = vca_config["apt_mirror"]
184         else:
185 0             self.apt_mirror = None
186
187 0         self.cloud = vca_config.get("cloud")
188         # self.log.debug('Arguments have been checked')
189
190         # juju data
191 0         self.controller = None  # it will be filled when connect to juju
192 0         self.juju_models = {}  # model objects for every model_name
193 0         self.juju_observers = {}  # model observers for every model_name
194 0         self._connecting = (
195             False  # while connecting to juju (to avoid duplicate connections)
196         )
197 0         self._authenticated = (
198             False  # it will be True when juju connection be stablished
199         )
200 0         self._creating_model = False  # True during model creation
201
202         # create juju pub key file in lcm container at
203         # ./local/share/juju/ssh/juju_id_rsa.pub
204 0         self._create_juju_public_key()
205
206 0         self.log.info("N2VC juju connector initialized")
207
208 0     async def get_status(self, namespace: str, yaml_format: bool = True):
209
210         # self.log.info('Getting NS status. namespace: {}'.format(namespace))
211
212 0         if not self._authenticated:
213 0             await self._juju_login()
214
215 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
216             namespace=namespace
217         )
218         # model name is ns_id
219 0         model_name = ns_id
220 0         if model_name is None:
221 0             msg = "Namespace {} not valid".format(namespace)
222 0             self.log.error(msg)
223 0             raise N2VCBadArgumentsException(msg, ["namespace"])
224
225         # get juju model (create model if needed)
226 0         model = await self._juju_get_model(model_name=model_name)
227
228 0         status = await model.get_status()
229
230 0         if yaml_format:
231 0             return obj_to_yaml(status)
232         else:
233 0             return obj_to_dict(status)
234
235 0     async def create_execution_environment(
236         self,
237         namespace: str,
238         db_dict: dict,
239         reuse_ee_id: str = None,
240         progress_timeout: float = None,
241         total_timeout: float = None,
242     ) -> (str, dict):
243
244 0         self.log.info(
245             "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
246                 namespace, reuse_ee_id
247             )
248         )
249
250 0         if not self._authenticated:
251 0             await self._juju_login()
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             machine = await self._juju_create_machine(
280                 model_name=model_name,
281                 application_name=application_name,
282                 machine_id=machine_id,
283                 db_dict=db_dict,
284                 progress_timeout=progress_timeout,
285                 total_timeout=total_timeout,
286             )
287 0         except Exception as e:
288 0             message = "Error creating machine on juju: {}".format(e)
289 0             self.log.error(message)
290 0             raise N2VCException(message=message)
291
292         # id for the execution environment
293 0         ee_id = N2VCJujuConnector._build_ee_id(
294             model_name=model_name,
295             application_name=application_name,
296             machine_id=str(machine.entity_id),
297         )
298 0         self.log.debug("ee_id: {}".format(ee_id))
299
300         # new machine credentials
301 0         credentials = dict()
302 0         credentials["hostname"] = machine.dns_name
303
304 0         self.log.info(
305             "Execution environment created. ee_id: {}, credentials: {}".format(
306                 ee_id, credentials
307             )
308         )
309
310 0         return ee_id, credentials
311
312 0     async def register_execution_environment(
313         self,
314         namespace: str,
315         credentials: dict,
316         db_dict: dict,
317         progress_timeout: float = None,
318         total_timeout: float = None,
319     ) -> str:
320
321 0         if not self._authenticated:
322 0             await self._juju_login()
323
324 0         self.log.info(
325             "Registering execution environment. namespace={}, credentials={}".format(
326                 namespace, credentials
327             )
328         )
329
330 0         if credentials is None:
331 0             raise N2VCBadArgumentsException(
332                 message="credentials are mandatory", bad_args=["credentials"]
333             )
334 0         if credentials.get("hostname"):
335 0             hostname = credentials["hostname"]
336         else:
337 0             raise N2VCBadArgumentsException(
338                 message="hostname is mandatory", bad_args=["credentials.hostname"]
339             )
340 0         if credentials.get("username"):
341 0             username = credentials["username"]
342         else:
343 0             raise N2VCBadArgumentsException(
344                 message="username is mandatory", bad_args=["credentials.username"]
345             )
346 0         if "private_key_path" in credentials:
347 0             private_key_path = credentials["private_key_path"]
348         else:
349             # if not passed as argument, use generated private key path
350 0             private_key_path = self.private_key_path
351
352 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
353             namespace=namespace
354         )
355
356         # model name
357 0         model_name = ns_id
358         # application name
359 0         application_name = self._get_application_name(namespace=namespace)
360
361         # register machine on juju
362 0         try:
363 0             machine_id = await self._juju_provision_machine(
364                 model_name=model_name,
365                 hostname=hostname,
366                 username=username,
367                 private_key_path=private_key_path,
368                 db_dict=db_dict,
369                 progress_timeout=progress_timeout,
370                 total_timeout=total_timeout,
371             )
372 0         except Exception as e:
373 0             self.log.error("Error registering machine: {}".format(e))
374 0             raise N2VCException(
375                 message="Error registering machine on juju: {}".format(e)
376             )
377
378 0         self.log.info("Machine registered: {}".format(machine_id))
379
380         # id for the execution environment
381 0         ee_id = N2VCJujuConnector._build_ee_id(
382             model_name=model_name,
383             application_name=application_name,
384             machine_id=str(machine_id),
385         )
386
387 0         self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
388
389 0         return ee_id
390
391 0     async def install_configuration_sw(
392         self,
393         ee_id: str,
394         artifact_path: str,
395         db_dict: dict,
396         progress_timeout: float = None,
397         total_timeout: float = None,
398         config: dict = None,
399     ):
400
401 0         self.log.info(
402             (
403                 "Installing configuration sw on ee_id: {}, "
404                 "artifact path: {}, db_dict: {}"
405             ).format(ee_id, artifact_path, db_dict)
406         )
407
408 0         if not self._authenticated:
409 0             await self._juju_login()
410
411         # check arguments
412 0         if ee_id is None or len(ee_id) == 0:
413 0             raise N2VCBadArgumentsException(
414                 message="ee_id is mandatory", bad_args=["ee_id"]
415             )
416 0         if artifact_path is None or len(artifact_path) == 0:
417 0             raise N2VCBadArgumentsException(
418                 message="artifact_path is mandatory", bad_args=["artifact_path"]
419             )
420 0         if db_dict is None:
421 0             raise N2VCBadArgumentsException(
422                 message="db_dict is mandatory", bad_args=["db_dict"]
423             )
424
425 0         try:
426 0             (
427                 model_name,
428                 application_name,
429                 machine_id,
430             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
431 0             self.log.debug(
432                 "model: {}, application: {}, machine: {}".format(
433                     model_name, application_name, machine_id
434                 )
435             )
436 0         except Exception:
437 0             raise N2VCBadArgumentsException(
438                 message="ee_id={} is not a valid execution environment id".format(
439                     ee_id
440                 ),
441                 bad_args=["ee_id"],
442             )
443
444         # remove // in charm path
445 0         while artifact_path.find("//") >= 0:
446 0             artifact_path = artifact_path.replace("//", "/")
447
448         # check charm path
449 0         if not self.fs.file_exists(artifact_path, mode="dir"):
450 0             msg = "artifact path does not exist: {}".format(artifact_path)
451 0             raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
452
453 0         if artifact_path.startswith("/"):
454 0             full_path = self.fs.path + artifact_path
455         else:
456 0             full_path = self.fs.path + "/" + artifact_path
457
458 0         try:
459 0             await self._juju_deploy_charm(
460                 model_name=model_name,
461                 application_name=application_name,
462                 charm_path=full_path,
463                 machine_id=machine_id,
464                 db_dict=db_dict,
465                 progress_timeout=progress_timeout,
466                 total_timeout=total_timeout,
467                 config=config,
468             )
469 0         except Exception as e:
470 0             raise N2VCException(
471                 message="Error desploying charm into ee={} : {}".format(ee_id, e)
472             )
473
474 0         self.log.info("Configuration sw installed")
475
476 0     async def get_ee_ssh_public__key(
477         self,
478         ee_id: str,
479         db_dict: dict,
480         progress_timeout: float = None,
481         total_timeout: float = None,
482     ) -> str:
483
484 0         self.log.info(
485             (
486                 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
487             ).format(ee_id, db_dict)
488         )
489
490 0         if not self._authenticated:
491 0             await self._juju_login()
492
493         # check arguments
494 0         if ee_id is None or len(ee_id) == 0:
495 0             raise N2VCBadArgumentsException(
496                 message="ee_id is mandatory", bad_args=["ee_id"]
497             )
498 0         if db_dict is None:
499 0             raise N2VCBadArgumentsException(
500                 message="db_dict is mandatory", bad_args=["db_dict"]
501             )
502
503 0         try:
504 0             (
505                 model_name,
506                 application_name,
507                 machine_id,
508             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
509 0             self.log.debug(
510                 "model: {}, application: {}, machine: {}".format(
511                     model_name, application_name, machine_id
512                 )
513             )
514 0         except Exception:
515 0             raise N2VCBadArgumentsException(
516                 message="ee_id={} is not a valid execution environment id".format(
517                     ee_id
518                 ),
519                 bad_args=["ee_id"],
520             )
521
522         # try to execute ssh layer primitives (if exist):
523         #       generate-ssh-key
524         #       get-ssh-public-key
525
526 0         output = None
527
528         # execute action: generate-ssh-key
529 0         try:
530 0             output, _status = await self._juju_execute_action(
531                 model_name=model_name,
532                 application_name=application_name,
533                 action_name="generate-ssh-key",
534                 db_dict=db_dict,
535                 progress_timeout=progress_timeout,
536                 total_timeout=total_timeout,
537             )
538 0         except Exception as e:
539 0             self.log.info(
540                 "Skipping exception while executing action generate-ssh-key: {}".format(
541                     e
542                 )
543             )
544
545         # execute action: get-ssh-public-key
546 0         try:
547 0             output, _status = await self._juju_execute_action(
548                 model_name=model_name,
549                 application_name=application_name,
550                 action_name="get-ssh-public-key",
551                 db_dict=db_dict,
552                 progress_timeout=progress_timeout,
553                 total_timeout=total_timeout,
554             )
555 0         except Exception as e:
556 0             msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
557 0             self.log.info(msg)
558 0             raise N2VCException(msg)
559
560         # return public key if exists
561 0         return output["pubkey"] if "pubkey" in output else output
562
563 0     async def add_relation(
564         self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
565     ):
566
567 0         self.log.debug(
568             "adding new relation between {} and {}, endpoints: {}, {}".format(
569                 ee_id_1, ee_id_2, endpoint_1, endpoint_2
570             )
571         )
572
573         # check arguments
574 0         if not ee_id_1:
575 0             message = "EE 1 is mandatory"
576 0             self.log.error(message)
577 0             raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
578 0         if not ee_id_2:
579 0             message = "EE 2 is mandatory"
580 0             self.log.error(message)
581 0             raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
582 0         if not endpoint_1:
583 0             message = "endpoint 1 is mandatory"
584 0             self.log.error(message)
585 0             raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
586 0         if not endpoint_2:
587 0             message = "endpoint 2 is mandatory"
588 0             self.log.error(message)
589 0             raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
590
591 0         if not self._authenticated:
592 0             await self._juju_login()
593
594         # get the model, the applications and the machines from the ee_id's
595 0         model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
596 0         model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
597
598         # model must be the same
599 0         if model_1 != model_2:
600 0             message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
601 0             self.log.error(message)
602 0             raise N2VCBadArgumentsException(
603                 message=message, bad_args=["ee_id_1", "ee_id_2"]
604             )
605
606         # add juju relations between two applications
607 0         try:
608 0             await self._juju_add_relation(
609                 model_name=model_1,
610                 application_name_1=app_1,
611                 application_name_2=app_2,
612                 relation_1=endpoint_1,
613                 relation_2=endpoint_2,
614             )
615 0         except Exception as e:
616 0             message = "Error adding relation between {} and {}: {}".format(
617                 ee_id_1, ee_id_2, e
618             )
619 0             self.log.error(message)
620 0             raise N2VCException(message=message)
621
622 0     async def remove_relation(self):
623 0         if not self._authenticated:
624 0             await self._juju_login()
625         # TODO
626 0         self.log.info("Method not implemented yet")
627 0         raise MethodNotImplemented()
628
629 0     async def deregister_execution_environments(self):
630 0         if not self._authenticated:
631 0             await self._juju_login()
632         # TODO
633 0         self.log.info("Method not implemented yet")
634 0         raise MethodNotImplemented()
635
636 0     async def delete_namespace(
637         self, namespace: str, db_dict: dict = None, total_timeout: float = None
638     ):
639 0         self.log.info("Deleting namespace={}".format(namespace))
640
641 0         if not self._authenticated:
642 0             await self._juju_login()
643
644         # check arguments
645 0         if namespace is None:
646 0             raise N2VCBadArgumentsException(
647                 message="namespace is mandatory", bad_args=["namespace"]
648             )
649
650 0         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
651             namespace=namespace
652         )
653 0         if ns_id is not None:
654 0             try:
655 0                 await self._juju_destroy_model(
656                     model_name=ns_id, total_timeout=total_timeout
657                 )
658 0             except N2VCNotFound:
659 0                 raise
660 0             except Exception as e:
661 0                 raise N2VCException(
662                     message="Error deleting namespace {} : {}".format(namespace, e)
663                 )
664         else:
665 0             raise N2VCBadArgumentsException(
666                 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
667             )
668
669 0         self.log.info("Namespace {} deleted".format(namespace))
670
671 0     async def delete_execution_environment(
672         self, ee_id: str, db_dict: dict = None, total_timeout: float = None
673     ):
674 0         self.log.info("Deleting execution environment ee_id={}".format(ee_id))
675
676 0         if not self._authenticated:
677 0             await self._juju_login()
678
679         # check arguments
680 0         if ee_id is None:
681 0             raise N2VCBadArgumentsException(
682                 message="ee_id is mandatory", bad_args=["ee_id"]
683             )
684
685 0         model_name, application_name, _machine_id = self._get_ee_id_components(
686             ee_id=ee_id
687         )
688
689         # destroy the application
690 0         try:
691 0             await self._juju_destroy_application(
692                 model_name=model_name, application_name=application_name
693             )
694 0         except Exception as e:
695 0             raise N2VCException(
696                 message=(
697                     "Error deleting execution environment {} (application {}) : {}"
698                 ).format(ee_id, application_name, e)
699             )
700
701         # destroy the machine
702         # try:
703         #     await self._juju_destroy_machine(
704         #         model_name=model_name,
705         #         machine_id=machine_id,
706         #         total_timeout=total_timeout
707         #     )
708         # except Exception as e:
709         #     raise N2VCException(
710         #        message='Error deleting execution environment {} (machine {}) : {}'
711         #                .format(ee_id, machine_id, e))
712
713 0         self.log.info("Execution environment {} deleted".format(ee_id))
714
715 0     async def exec_primitive(
716         self,
717         ee_id: str,
718         primitive_name: str,
719         params_dict: dict,
720         db_dict: dict = None,
721         progress_timeout: float = None,
722         total_timeout: float = None,
723     ) -> str:
724
725 0         self.log.info(
726             "Executing primitive: {} on ee: {}, params: {}".format(
727                 primitive_name, ee_id, params_dict
728             )
729         )
730
731 0         if not self._authenticated:
732 0             await self._juju_login()
733
734         # check arguments
735 0         if ee_id is None or len(ee_id) == 0:
736 0             raise N2VCBadArgumentsException(
737                 message="ee_id is mandatory", bad_args=["ee_id"]
738             )
739 0         if primitive_name is None or len(primitive_name) == 0:
740 0             raise N2VCBadArgumentsException(
741                 message="action_name is mandatory", bad_args=["action_name"]
742             )
743 0         if params_dict is None:
744 0             params_dict = dict()
745
746 0         try:
747 0             (
748                 model_name,
749                 application_name,
750                 _machine_id,
751             ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
752 0         except Exception:
753 0             raise N2VCBadArgumentsException(
754                 message="ee_id={} is not a valid execution environment id".format(
755                     ee_id
756                 ),
757                 bad_args=["ee_id"],
758             )
759
760 0         if primitive_name == "config":
761             # Special case: config primitive
762 0             try:
763 0                 await self._juju_configure_application(
764                     model_name=model_name,
765                     application_name=application_name,
766                     config=params_dict,
767                     db_dict=db_dict,
768                     progress_timeout=progress_timeout,
769                     total_timeout=total_timeout,
770                 )
771 0             except Exception as e:
772 0                 self.log.error("Error configuring juju application: {}".format(e))
773 0                 raise N2VCExecutionException(
774                     message="Error configuring application into ee={} : {}".format(
775                         ee_id, e
776                     ),
777                     primitive_name=primitive_name,
778                 )
779 0             return "CONFIG OK"
780         else:
781 0             try:
782 0                 output, status = await self._juju_execute_action(
783                     model_name=model_name,
784                     application_name=application_name,
785                     action_name=primitive_name,
786                     db_dict=db_dict,
787                     progress_timeout=progress_timeout,
788                     total_timeout=total_timeout,
789                     **params_dict
790                 )
791 0                 if status == "completed":
792 0                     return output
793                 else:
794 0                     raise Exception("status is not completed: {}".format(status))
795 0             except Exception as e:
796 0                 self.log.error(
797                     "Error executing primitive {}: {}".format(primitive_name, e)
798                 )
799 0                 raise N2VCExecutionException(
800                     message="Error executing primitive {} into ee={} : {}".format(
801                         primitive_name, ee_id, e
802                     ),
803                     primitive_name=primitive_name,
804                 )
805
806 0     async def disconnect(self):
807 0         self.log.info("closing juju N2VC...")
808 0         await self._juju_logout()
809
810     """
811     ####################################################################################
812     ################################### P R I V A T E ##################################
813     ####################################################################################
814     """
815
816 0     def _write_ee_id_db(self, db_dict: dict, ee_id: str):
817
818         # write ee_id to database: _admin.deployed.VCA.x
819 0         try:
820 0             the_table = db_dict["collection"]
821 0             the_filter = db_dict["filter"]
822 0             the_path = db_dict["path"]
823 0             if not the_path[-1] == ".":
824 0                 the_path = the_path + "."
825 0             update_dict = {the_path + "ee_id": ee_id}
826             # self.log.debug('Writing ee_id to database: {}'.format(the_path))
827 0             self.db.set_one(
828                 table=the_table,
829                 q_filter=the_filter,
830                 update_dict=update_dict,
831                 fail_on_empty=True,
832             )
833 0         except asyncio.CancelledError:
834 0             raise
835 0         except Exception as e:
836 0             self.log.error("Error writing ee_id to database: {}".format(e))
837
838 0     @staticmethod
839 0     def _build_ee_id(model_name: str, application_name: str, machine_id: str):
840         """
841         Build an execution environment id form model, application and machine
842         :param model_name:
843         :param application_name:
844         :param machine_id:
845         :return:
846         """
847         # id for the execution environment
848 0         return "{}.{}.{}".format(model_name, application_name, machine_id)
849
850 0     @staticmethod
851 0     def _get_ee_id_components(ee_id: str) -> (str, str, str):
852         """
853         Get model, application and machine components from an execution environment id
854         :param ee_id:
855         :return: model_name, application_name, machine_id
856         """
857
858 0         if ee_id is None:
859 0             return None, None, None
860
861         # split components of id
862 0         parts = ee_id.split(".")
863 0         model_name = parts[0]
864 0         application_name = parts[1]
865 0         machine_id = parts[2]
866 0         return model_name, application_name, machine_id
867
868 0     def _get_application_name(self, namespace: str) -> str:
869         """
870         Build application name from namespace
871         :param namespace:
872         :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
873         """
874
875         # TODO: Enforce the Juju 50-character application limit
876
877         # split namespace components
878 0         _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
879             namespace=namespace
880         )
881
882 0         if vnf_id is None or len(vnf_id) == 0:
883 0             vnf_id = ""
884         else:
885             # Shorten the vnf_id to its last twelve characters
886 0             vnf_id = "vnf-" + vnf_id[-12:]
887
888 0         if vdu_id is None or len(vdu_id) == 0:
889 0             vdu_id = ""
890         else:
891             # Shorten the vdu_id to its last twelve characters
892 0             vdu_id = "-vdu-" + vdu_id[-12:]
893
894 0         if vdu_count is None or len(vdu_count) == 0:
895 0             vdu_count = ""
896         else:
897 0             vdu_count = "-cnt-" + vdu_count
898
899 0         application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
900
901 0         return N2VCJujuConnector._format_app_name(application_name)
902
903 0     async def _juju_create_machine(
904         self,
905         model_name: str,
906         application_name: str,
907         machine_id: str = None,
908         db_dict: dict = None,
909         progress_timeout: float = None,
910         total_timeout: float = None,
911     ) -> Machine:
912
913 0         self.log.debug(
914             "creating machine in model: {}, existing machine id: {}".format(
915                 model_name, machine_id
916             )
917         )
918
919         # get juju model and observer (create model if needed)
920 0         model = await self._juju_get_model(model_name=model_name)
921 0         observer = self.juju_observers[model_name]
922
923         # find machine id in model
924 0         machine = None
925 0         if machine_id is not None:
926 0             self.log.debug("Finding existing machine id {} in model".format(machine_id))
927             # get juju existing machines in the model
928 0             existing_machines = await model.get_machines()
929 0             if machine_id in existing_machines:
930 0                 self.log.debug(
931                     "Machine id {} found in model (reusing it)".format(machine_id)
932                 )
933 0                 machine = model.machines[machine_id]
934
935 0         if machine is None:
936 0             self.log.debug("Creating a new machine in juju...")
937             # machine does not exist, create it and wait for it
938 0             machine = await model.add_machine(
939                 spec=None, constraints=None, disks=None, series="xenial"
940             )
941
942             # register machine with observer
943 0             observer.register_machine(machine=machine, db_dict=db_dict)
944
945             # id for the execution environment
946 0             ee_id = N2VCJujuConnector._build_ee_id(
947                 model_name=model_name,
948                 application_name=application_name,
949                 machine_id=str(machine.entity_id),
950             )
951
952             # write ee_id in database
953 0             self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
954
955             # wait for machine creation
956 0             await observer.wait_for_machine(
957                 machine_id=str(machine.entity_id),
958                 progress_timeout=progress_timeout,
959                 total_timeout=total_timeout,
960             )
961
962         else:
963
964 0             self.log.debug("Reusing old machine pending")
965
966             # register machine with observer
967 0             observer.register_machine(machine=machine, db_dict=db_dict)
968
969             # machine does exist, but it is in creation process (pending), wait for
970             # create finalisation
971 0             await observer.wait_for_machine(
972                 machine_id=machine.entity_id,
973                 progress_timeout=progress_timeout,
974                 total_timeout=total_timeout,
975             )
976
977 0         self.log.debug("Machine ready at " + str(machine.dns_name))
978 0         return machine
979
980 0     async def _juju_provision_machine(
981         self,
982         model_name: str,
983         hostname: str,
984         username: str,
985         private_key_path: str,
986         db_dict: dict = None,
987         progress_timeout: float = None,
988         total_timeout: float = None,
989     ) -> str:
990
991 0         if not self.api_proxy:
992 0             msg = "Cannot provision machine: api_proxy is not defined"
993 0             self.log.error(msg=msg)
994 0             raise N2VCException(message=msg)
995
996 0         self.log.debug(
997             "provisioning machine. model: {}, hostname: {}, username: {}".format(
998                 model_name, hostname, username
999             )
1000         )
1001
1002 0         if not self._authenticated:
1003 0             await self._juju_login()
1004
1005         # get juju model and observer
1006 0         model = await self._juju_get_model(model_name=model_name)
1007 0         observer = self.juju_observers[model_name]
1008
1009         # TODO check if machine is already provisioned
1010 0         machine_list = await model.get_machines()
1011
1012 0         provisioner = AsyncSSHProvisioner(
1013             host=hostname,
1014             user=username,
1015             private_key_path=private_key_path,
1016             log=self.log,
1017         )
1018
1019 0         params = None
1020 0         try:
1021 0             params = await provisioner.provision_machine()
1022 0         except Exception as ex:
1023 0             msg = "Exception provisioning machine: {}".format(ex)
1024 0             self.log.error(msg)
1025 0             raise N2VCException(message=msg)
1026
1027 0         params.jobs = ["JobHostUnits"]
1028
1029 0         connection = model.connection()
1030
1031         # Submit the request.
1032 0         self.log.debug("Adding machine to model")
1033 0         client_facade = client.ClientFacade.from_connection(connection)
1034 0         results = await client_facade.AddMachines(params=[params])
1035 0         error = results.machines[0].error
1036 0         if error:
1037 0             msg = "Error adding machine: {}".format(error.message)
1038 0             self.log.error(msg=msg)
1039 0             raise ValueError(msg)
1040
1041 0         machine_id = results.machines[0].machine
1042
1043         # Need to run this after AddMachines has been called,
1044         # as we need the machine_id
1045 0         self.log.debug("Installing Juju agent into machine {}".format(machine_id))
1046 0         asyncio.ensure_future(
1047             provisioner.install_agent(
1048                 connection=connection,
1049                 nonce=params.nonce,
1050                 machine_id=machine_id,
1051                 api=self.api_proxy,
1052             )
1053         )
1054
1055         # wait for machine in model (now, machine is not yet in model, so we must
1056         # wait for it)
1057 0         machine = None
1058 0         for _ in range(10):
1059 0             machine_list = await model.get_machines()
1060 0             if machine_id in machine_list:
1061 0                 self.log.debug("Machine {} found in model!".format(machine_id))
1062 0                 machine = model.machines.get(machine_id)
1063 0                 break
1064 0             await asyncio.sleep(2)
1065
1066 0         if machine is None:
1067 0             msg = "Machine {} not found in model".format(machine_id)
1068 0             self.log.error(msg=msg)
1069 0             raise Exception(msg)
1070
1071         # register machine with observer
1072 0         observer.register_machine(machine=machine, db_dict=db_dict)
1073
1074         # wait for machine creation
1075 0         self.log.debug("waiting for provision finishes... {}".format(machine_id))
1076 0         await observer.wait_for_machine(
1077             machine_id=machine_id,
1078             progress_timeout=progress_timeout,
1079             total_timeout=total_timeout,
1080         )
1081
1082 0         self.log.debug("Machine provisioned {}".format(machine_id))
1083
1084 0         return machine_id
1085
1086 0     async def _juju_deploy_charm(
1087         self,
1088         model_name: str,
1089         application_name: str,
1090         charm_path: str,
1091         machine_id: str,
1092         db_dict: dict,
1093         progress_timeout: float = None,
1094         total_timeout: float = None,
1095         config: dict = None,
1096     ) -> (Application, int):
1097
1098         # get juju model and observer
1099 0         model = await self._juju_get_model(model_name=model_name)
1100 0         observer = self.juju_observers[model_name]
1101
1102         # check if application already exists
1103 0         application = None
1104 0         if application_name in model.applications:
1105 0             application = model.applications[application_name]
1106
1107 0         if application is None:
1108
1109             # application does not exist, create it and wait for it
1110 0             self.log.debug(
1111                 "deploying application {} to machine {}, model {}".format(
1112                     application_name, machine_id, model_name
1113                 )
1114             )
1115 0             self.log.debug("charm: {}".format(charm_path))
1116 0             machine = model.machines[machine_id]
1117             # series = None
1118 0             application = await model.deploy(
1119                 entity_url=charm_path,
1120                 application_name=application_name,
1121                 channel="stable",
1122                 num_units=1,
1123                 series=machine.series,
1124                 to=machine_id,
1125                 config=config,
1126             )
1127
1128             # register application with observer
1129 0             observer.register_application(application=application, db_dict=db_dict)
1130
1131 0             self.log.debug(
1132                 "waiting for application deployed... {}".format(application.entity_id)
1133             )
1134 0             retries = await observer.wait_for_application(
1135                 application_id=application.entity_id,
1136                 progress_timeout=progress_timeout,
1137                 total_timeout=total_timeout,
1138             )
1139 0             self.log.debug("application deployed")
1140
1141         else:
1142
1143             # register application with observer
1144 0             observer.register_application(application=application, db_dict=db_dict)
1145
1146             # application already exists, but not finalised
1147 0             self.log.debug("application already exists, waiting for deployed...")
1148 0             retries = await observer.wait_for_application(
1149                 application_id=application.entity_id,
1150                 progress_timeout=progress_timeout,
1151                 total_timeout=total_timeout,
1152             )
1153 0             self.log.debug("application deployed")
1154
1155 0         return application, retries
1156
1157 0     async def _juju_execute_action(
1158         self,
1159         model_name: str,
1160         application_name: str,
1161         action_name: str,
1162         db_dict: dict,
1163         progress_timeout: float = None,
1164         total_timeout: float = None,
1165         **kwargs
1166     ) -> Action:
1167
1168         # get juju model and observer
1169 0         model = await self._juju_get_model(model_name=model_name)
1170 0         observer = self.juju_observers[model_name]
1171
1172 0         application = await self._juju_get_application(
1173             model_name=model_name, application_name=application_name
1174         )
1175
1176 0         unit = None
1177 0         for u in application.units:
1178 0             if await u.is_leader_from_status():
1179 0                 unit = u
1180 0         if unit is not None:
1181 0             actions = await application.get_actions()
1182 0             if action_name in actions:
1183 0                 self.log.debug(
1184                     'executing action "{}" using params: {}'.format(action_name, kwargs)
1185                 )
1186 0                 action = await unit.run_action(action_name, **kwargs)
1187
1188                 # register action with observer
1189 0                 observer.register_action(action=action, db_dict=db_dict)
1190
1191 0                 await observer.wait_for_action(
1192                     action_id=action.entity_id,
1193                     progress_timeout=progress_timeout,
1194                     total_timeout=total_timeout,
1195                 )
1196 0                 self.log.debug("action completed with status: {}".format(action.status))
1197 0                 output = await model.get_action_output(action_uuid=action.entity_id)
1198 0                 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1199 0                 if action.entity_id in status:
1200 0                     status = status[action.entity_id]
1201                 else:
1202 0                     status = "failed"
1203 0                 return output, status
1204
1205 0         raise N2VCExecutionException(
1206             message="Cannot execute action on charm", primitive_name=action_name
1207         )
1208
1209 0     async def _juju_configure_application(
1210         self,
1211         model_name: str,
1212         application_name: str,
1213         config: dict,
1214         db_dict: dict,
1215         progress_timeout: float = None,
1216         total_timeout: float = None,
1217     ):
1218
1219         # get the application
1220 0         application = await self._juju_get_application(
1221             model_name=model_name, application_name=application_name
1222         )
1223
1224 0         self.log.debug(
1225             "configuring the application {} -> {}".format(application_name, config)
1226         )
1227 0         res = await application.set_config(config)
1228 0         self.log.debug(
1229             "application {} configured. res={}".format(application_name, res)
1230         )
1231
1232         # Verify the config is set
1233 0         new_conf = await application.get_config()
1234 0         for key in config:
1235 0             value = new_conf[key]["value"]
1236 0             self.log.debug("    {} = {}".format(key, value))
1237 0             if config[key] != value:
1238 0                 raise N2VCException(
1239                     message="key {} is not configured correctly {} != {}".format(
1240                         key, config[key], new_conf[key]
1241                     )
1242                 )
1243
1244         # check if 'verify-ssh-credentials' action exists
1245         # unit = application.units[0]
1246 0         actions = await application.get_actions()
1247 0         if "verify-ssh-credentials" not in actions:
1248 0             msg = (
1249                 "Action verify-ssh-credentials does not exist in application {}"
1250             ).format(application_name)
1251 0             self.log.debug(msg=msg)
1252 0             return False
1253
1254         # execute verify-credentials
1255 0         num_retries = 20
1256 0         retry_timeout = 15.0
1257 0         for _ in range(num_retries):
1258 0             try:
1259 0                 self.log.debug("Executing action verify-ssh-credentials...")
1260 0                 output, ok = await self._juju_execute_action(
1261                     model_name=model_name,
1262                     application_name=application_name,
1263                     action_name="verify-ssh-credentials",
1264                     db_dict=db_dict,
1265                     progress_timeout=progress_timeout,
1266                     total_timeout=total_timeout,
1267                 )
1268 0                 self.log.debug("Result: {}, output: {}".format(ok, output))
1269 0                 return True
1270 0             except asyncio.CancelledError:
1271 0                 raise
1272 0             except Exception as e:
1273 0                 self.log.debug(
1274                     "Error executing verify-ssh-credentials: {}. Retrying...".format(e)
1275                 )
1276 0                 await asyncio.sleep(retry_timeout)
1277         else:
1278 0             self.log.error(
1279                 "Error executing verify-ssh-credentials after {} retries. ".format(
1280                     num_retries
1281                 )
1282             )
1283 0             return False
1284
1285 0     async def _juju_get_application(self, model_name: str, application_name: str):
1286         """Get the deployed application."""
1287
1288 0         model = await self._juju_get_model(model_name=model_name)
1289
1290 0         application_name = N2VCJujuConnector._format_app_name(application_name)
1291
1292 0         if model.applications and application_name in model.applications:
1293 0             return model.applications[application_name]
1294         else:
1295 0             raise N2VCException(
1296                 message="Cannot get application {} from model {}".format(
1297                     application_name, model_name
1298                 )
1299             )
1300
1301 0     async def _juju_get_model(self, model_name: str) -> Model:
1302         """ Get a model object from juju controller
1303         If the model does not exits, it creates it.
1304
1305         :param str model_name: name of the model
1306         :returns Model: model obtained from juju controller or Exception
1307         """
1308
1309         # format model name
1310 0         model_name = N2VCJujuConnector._format_model_name(model_name)
1311
1312 0         if model_name in self.juju_models:
1313 0             return self.juju_models[model_name]
1314
1315 0         if self._creating_model:
1316 0             self.log.debug("Another coroutine is creating a model. Wait...")
1317 0         while self._creating_model:
1318             # another coroutine is creating a model, wait
1319 0             await asyncio.sleep(0.1)
1320             # retry (perhaps another coroutine has created the model meanwhile)
1321 0             if model_name in self.juju_models:
1322 0                 return self.juju_models[model_name]
1323
1324 0         try:
1325 0             self._creating_model = True
1326
1327             # get juju model names from juju
1328 0             model_list = await self.controller.list_models()
1329
1330 0             if model_name not in model_list:
1331 0                 self.log.info(
1332                     "Model {} does not exist. Creating new model...".format(model_name)
1333                 )
1334 0                 config_dict = {"authorized-keys": self.public_key}
1335 0                 if self.apt_mirror:
1336 0                     config_dict["apt-mirror"] = self.apt_mirror
1337 0                 if not self.enable_os_upgrade:
1338 0                     config_dict["enable-os-refresh-update"] = False
1339 0                     config_dict["enable-os-upgrade"] = False
1340 0                 if self.cloud in self.BUILT_IN_CLOUDS:
1341 0                     model = await self.controller.add_model(
1342                         model_name=model_name,
1343                         config=config_dict,
1344                         cloud_name=self.cloud,
1345                     )
1346                 else:
1347 0                     model = await self.controller.add_model(
1348                         model_name=model_name,
1349                         config=config_dict,
1350                         cloud_name=self.cloud,
1351                         credential_name=self.cloud,
1352                     )
1353 0                 self.log.info("New model created, name={}".format(model_name))
1354             else:
1355 0                 self.log.debug(
1356                     "Model already exists in juju. Getting model {}".format(model_name)
1357                 )
1358 0                 model = await self.controller.get_model(model_name)
1359 0                 self.log.debug("Existing model in juju, name={}".format(model_name))
1360
1361 0             self.juju_models[model_name] = model
1362 0             self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1363 0             return model
1364
1365 0         except Exception as e:
1366 0             msg = "Cannot get model {}. Exception: {}".format(model_name, e)
1367 0             self.log.error(msg)
1368 0             raise N2VCException(msg)
1369         finally:
1370 0             self._creating_model = False
1371
1372 0     async def _juju_add_relation(
1373         self,
1374         model_name: str,
1375         application_name_1: str,
1376         application_name_2: str,
1377         relation_1: str,
1378         relation_2: str,
1379     ):
1380
1381         # get juju model and observer
1382 0         model = await self._juju_get_model(model_name=model_name)
1383
1384 0         r1 = "{}:{}".format(application_name_1, relation_1)
1385 0         r2 = "{}:{}".format(application_name_2, relation_2)
1386
1387 0         self.log.debug("adding relation: {} -> {}".format(r1, r2))
1388 0         try:
1389 0             await model.add_relation(relation1=r1, relation2=r2)
1390 0         except JujuAPIError as e:
1391             # If one of the applications in the relationship doesn't exist, or the
1392             # relation has already been added,
1393             # let the operation fail silently.
1394 0             if "not found" in e.message:
1395 0                 return
1396 0             if "already exists" in e.message:
1397 0                 return
1398             # another execption, raise it
1399 0             raise e
1400
1401 0     async def _juju_destroy_application(self, model_name: str, application_name: str):
1402
1403 0         self.log.debug(
1404             "Destroying application {} in model {}".format(application_name, model_name)
1405         )
1406
1407         # get juju model and observer
1408 0         model = await self._juju_get_model(model_name=model_name)
1409 0         observer = self.juju_observers[model_name]
1410
1411 0         application = model.applications.get(application_name)
1412 0         if application:
1413 0             observer.unregister_application(application_name)
1414 0             await application.destroy()
1415         else:
1416 0             self.log.debug("Application not found: {}".format(application_name))
1417
1418 0     async def _juju_destroy_machine(
1419         self, model_name: str, machine_id: str, total_timeout: float = None
1420     ):
1421
1422 0         self.log.debug(
1423             "Destroying machine {} in model {}".format(machine_id, model_name)
1424         )
1425
1426 0         if total_timeout is None:
1427 0             total_timeout = 3600
1428
1429         # get juju model and observer
1430 0         model = await self._juju_get_model(model_name=model_name)
1431 0         observer = self.juju_observers[model_name]
1432
1433 0         machines = await model.get_machines()
1434 0         if machine_id in machines:
1435 0             machine = model.machines[machine_id]
1436 0             observer.unregister_machine(machine_id)
1437             # TODO: change this by machine.is_manual when this is upstreamed:
1438             # https://github.com/juju/python-libjuju/pull/396
1439 0             if "instance-id" in machine.safe_data and machine.safe_data[
1440                 "instance-id"
1441             ].startswith("manual:"):
1442 0                 self.log.debug("machine.destroy(force=True) started.")
1443 0                 await machine.destroy(force=True)
1444 0                 self.log.debug("machine.destroy(force=True) passed.")
1445                 # max timeout
1446 0                 end = time.time() + total_timeout
1447                 # wait for machine removal
1448 0                 machines = await model.get_machines()
1449 0                 while machine_id in machines and time.time() < end:
1450 0                     self.log.debug(
1451                         "Waiting for machine {} is destroyed".format(machine_id)
1452                     )
1453 0                     await asyncio.sleep(0.5)
1454 0                     machines = await model.get_machines()
1455 0                 self.log.debug("Machine destroyed: {}".format(machine_id))
1456         else:
1457 0             self.log.debug("Machine not found: {}".format(machine_id))
1458
1459 0     async def _juju_destroy_model(self, model_name: str, total_timeout: float = None):
1460
1461 0         self.log.debug("Destroying model {}".format(model_name))
1462
1463 0         if total_timeout is None:
1464 0             total_timeout = 3600
1465 0         end = time.time() + total_timeout
1466
1467 0         model = await self._juju_get_model(model_name=model_name)
1468
1469 0         if not model:
1470 0             raise N2VCNotFound(message="Model {} does not exist".format(model_name))
1471
1472 0         uuid = model.info.uuid
1473
1474         # destroy applications
1475 0         for application_name in model.applications:
1476 0             try:
1477 0                 await self._juju_destroy_application(
1478                     model_name=model_name, application_name=application_name
1479                 )
1480 0             except Exception as e:
1481 0                 self.log.error(
1482                     "Error destroying application {} in model {}: {}".format(
1483                         application_name, model_name, e
1484                     )
1485                 )
1486
1487         # destroy machines
1488 0         machines = await model.get_machines()
1489 0         for machine_id in machines:
1490 0             try:
1491 0                 await self._juju_destroy_machine(
1492                     model_name=model_name, machine_id=machine_id
1493                 )
1494 0             except asyncio.CancelledError:
1495 0                 raise
1496 0             except Exception:
1497                 # ignore exceptions destroying machine
1498 0                 pass
1499
1500 0         await self._juju_disconnect_model(model_name=model_name)
1501
1502 0         self.log.debug("destroying model {}...".format(model_name))
1503 0         await self.controller.destroy_model(uuid)
1504         # self.log.debug('model destroy requested {}'.format(model_name))
1505
1506         # wait for model is completely destroyed
1507 0         self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
1508 0         last_exception = ""
1509 0         while time.time() < end:
1510 0             try:
1511                 # await self.controller.get_model(uuid)
1512 0                 models = await self.controller.list_models()
1513 0                 if model_name not in models:
1514 0                     self.log.debug(
1515                         "The model {} ({}) was destroyed".format(model_name, uuid)
1516                     )
1517 0                     return
1518 0             except asyncio.CancelledError:
1519 0                 raise
1520 0             except Exception as e:
1521 0                 last_exception = e
1522 0             await asyncio.sleep(5)
1523 0         raise N2VCException(
1524             "Timeout waiting for model {} to be destroyed {}".format(
1525                 model_name, last_exception
1526             )
1527         )
1528
1529 0     async def _juju_login(self):
1530         """Connect to juju controller
1531
1532         """
1533
1534         # if already authenticated, exit function
1535 0         if self._authenticated:
1536 0             return
1537
1538         # if connecting, wait for finish
1539         # another task could be trying to connect in parallel
1540 0         while self._connecting:
1541 0             await asyncio.sleep(0.1)
1542
1543         # double check after other task has finished
1544 0         if self._authenticated:
1545 0             return
1546
1547 0         try:
1548 0             self._connecting = True
1549 0             self.log.info(
1550                 "connecting to juju controller: {} {}:{}{}".format(
1551                     self.url,
1552                     self.username,
1553                     self.secret[:8] + "...",
1554                     " with ca_cert" if self.ca_cert else "",
1555                 )
1556             )
1557
1558             # Create controller object
1559 0             self.controller = Controller(loop=self.loop)
1560             # Connect to controller
1561 0             await self.controller.connect(
1562                 endpoint=self.url,
1563                 username=self.username,
1564                 password=self.secret,
1565                 cacert=self.ca_cert,
1566             )
1567 0             self._authenticated = True
1568 0             self.log.info("juju controller connected")
1569 0         except Exception as e:
1570 0             message = "Exception connecting to juju: {}".format(e)
1571 0             self.log.error(message)
1572 0             raise N2VCConnectionException(message=message, url=self.url)
1573         finally:
1574 0             self._connecting = False
1575
1576 0     async def _juju_logout(self):
1577         """Logout of the Juju controller."""
1578 0         if not self._authenticated:
1579 0             return False
1580
1581         # disconnect all models
1582 0         for model_name in self.juju_models:
1583 0             try:
1584 0                 await self._juju_disconnect_model(model_name)
1585 0             except Exception as e:
1586 0                 self.log.error(
1587                     "Error disconnecting model {} : {}".format(model_name, e)
1588                 )
1589                 # continue with next model...
1590
1591 0         self.log.info("Disconnecting controller")
1592 0         try:
1593 0             await self.controller.disconnect()
1594 0         except Exception as e:
1595 0             raise N2VCConnectionException(
1596                 message="Error disconnecting controller: {}".format(e), url=self.url
1597             )
1598
1599 0         self.controller = None
1600 0         self._authenticated = False
1601 0         self.log.info("disconnected")
1602
1603 0     async def _juju_disconnect_model(self, model_name: str):
1604 0         self.log.debug("Disconnecting model {}".format(model_name))
1605 0         if model_name in self.juju_models:
1606 0             await self.juju_models[model_name].disconnect()
1607 0             self.juju_models[model_name] = None
1608 0             self.juju_observers[model_name] = None
1609         else:
1610 0             self.warning("Cannot disconnect model: {}".format(model_name))
1611
1612 0     def _create_juju_public_key(self):
1613         """Recreate the Juju public key on lcm container, if needed
1614         Certain libjuju commands expect to be run from the same machine as Juju
1615          is bootstrapped to. This method will write the public key to disk in
1616          that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1617         """
1618
1619         # Make sure that we have a public key before writing to disk
1620 0         if self.public_key is None or len(self.public_key) == 0:
1621 0             if "OSMLCM_VCA_PUBKEY" in os.environ:
1622 0                 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1623 0                 if len(self.public_key) == 0:
1624 0                     return
1625             else:
1626 0                 return
1627
1628 0         pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1629 0         file_path = "{}/juju_id_rsa.pub".format(pk_path)
1630 0         self.log.debug(
1631             "writing juju public key to file:\n{}\npublic key: {}".format(
1632                 file_path, self.public_key
1633             )
1634         )
1635 0         if not os.path.exists(pk_path):
1636             # create path and write file
1637 0             os.makedirs(pk_path)
1638 0             with open(file_path, "w") as f:
1639 0                 self.log.debug("Creating juju public key file: {}".format(file_path))
1640 0                 f.write(self.public_key)
1641         else:
1642 0             self.log.debug("juju public key file already exists: {}".format(file_path))
1643
1644 0     @staticmethod
1645 0     def _format_model_name(name: str) -> str:
1646         """Format the name of the model.
1647
1648         Model names may only contain lowercase letters, digits and hyphens
1649         """
1650
1651 0         return name.replace("_", "-").replace(" ", "-").lower()
1652
1653 0     @staticmethod
1654 0     def _format_app_name(name: str) -> str:
1655         """Format the name of the application (in order to assure valid application name).
1656
1657         Application names have restrictions (run juju deploy --help):
1658             - contains lowercase letters 'a'-'z'
1659             - contains numbers '0'-'9'
1660             - contains hyphens '-'
1661             - starts with a lowercase letter
1662             - not two or more consecutive hyphens
1663             - after a hyphen, not a group with all numbers
1664         """
1665
1666 0         def all_numbers(s: str) -> bool:
1667 0             for c in s:
1668 0                 if not c.isdigit():
1669 0                     return False
1670 0             return True
1671
1672 0         new_name = name.replace("_", "-")
1673 0         new_name = new_name.replace(" ", "-")
1674 0         new_name = new_name.lower()
1675 0         while new_name.find("--") >= 0:
1676 0             new_name = new_name.replace("--", "-")
1677 0         groups = new_name.split("-")
1678
1679         # find 'all numbers' groups and prefix them with a letter
1680 0         app_name = ""
1681 0         for i in range(len(groups)):
1682 0             group = groups[i]
1683 0             if all_numbers(group):
1684 0                 group = "z" + group
1685 0             if i > 0:
1686 0                 app_name += "-"
1687 0             app_name += group
1688
1689 0         if app_name[0].isdigit():
1690 0             app_name = "z" + app_name
1691
1692 0         return app_name