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