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