Fix 1462
[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,
798 total_timeout=total_timeout,
799 )
800 else:
801 # destroy the application
802 await self.libjuju.destroy_application(
803 model_name=model_name,
804 application_name=application_name,
805 total_timeout=total_timeout,
806 )
807 except Exception as e:
808 raise N2VCException(
809 message=(
810 "Error deleting execution environment {} (application {}) : {}"
811 ).format(ee_id, application_name, e)
812 )
813
814 self.log.info("Execution environment {} deleted".format(ee_id))
815
816 async def exec_primitive(
817 self,
818 ee_id: str,
819 primitive_name: str,
820 params_dict: dict,
821 db_dict: dict = None,
822 progress_timeout: float = None,
823 total_timeout: float = None,
824 ) -> str:
825
826 self.log.info(
827 "Executing primitive: {} on ee: {}, params: {}".format(
828 primitive_name, ee_id, params_dict
829 )
830 )
831
832 # check arguments
833 if ee_id is None or len(ee_id) == 0:
834 raise N2VCBadArgumentsException(
835 message="ee_id is mandatory", bad_args=["ee_id"]
836 )
837 if primitive_name is None or len(primitive_name) == 0:
838 raise N2VCBadArgumentsException(
839 message="action_name is mandatory", bad_args=["action_name"]
840 )
841 if params_dict is None:
842 params_dict = dict()
843
844 try:
845 (
846 model_name,
847 application_name,
848 _machine_id,
849 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
850 except Exception:
851 raise N2VCBadArgumentsException(
852 message="ee_id={} is not a valid execution environment id".format(
853 ee_id
854 ),
855 bad_args=["ee_id"],
856 )
857
858 if primitive_name == "config":
859 # Special case: config primitive
860 try:
861 await self.libjuju.configure_application(
862 model_name=model_name,
863 application_name=application_name,
864 config=params_dict,
865 )
866 actions = await self.libjuju.get_actions(
867 application_name=application_name, model_name=model_name,
868 )
869 self.log.debug(
870 "Application {} has these actions: {}".format(
871 application_name, actions
872 )
873 )
874 if "verify-ssh-credentials" in actions:
875 # execute verify-credentials
876 num_retries = 20
877 retry_timeout = 15.0
878 for _ in range(num_retries):
879 try:
880 self.log.debug("Executing action verify-ssh-credentials...")
881 output, ok = await self.libjuju.execute_action(
882 model_name=model_name,
883 application_name=application_name,
884 action_name="verify-ssh-credentials",
885 db_dict=db_dict,
886 progress_timeout=progress_timeout,
887 total_timeout=total_timeout,
888 )
889
890 if ok == "failed":
891 self.log.debug(
892 "Error executing verify-ssh-credentials: {}. Retrying..."
893 )
894 await asyncio.sleep(retry_timeout)
895
896 continue
897 self.log.debug("Result: {}, output: {}".format(ok, output))
898 break
899 except asyncio.CancelledError:
900 raise
901 else:
902 self.log.error(
903 "Error executing verify-ssh-credentials after {} retries. ".format(
904 num_retries
905 )
906 )
907 else:
908 msg = "Action verify-ssh-credentials does not exist in application {}".format(
909 application_name
910 )
911 self.log.debug(msg=msg)
912 except Exception as e:
913 self.log.error("Error configuring juju application: {}".format(e))
914 raise N2VCExecutionException(
915 message="Error configuring application into ee={} : {}".format(
916 ee_id, e
917 ),
918 primitive_name=primitive_name,
919 )
920 return "CONFIG OK"
921 else:
922 try:
923 output, status = await self.libjuju.execute_action(
924 model_name=model_name,
925 application_name=application_name,
926 action_name=primitive_name,
927 db_dict=db_dict,
928 progress_timeout=progress_timeout,
929 total_timeout=total_timeout,
930 **params_dict
931 )
932 if status == "completed":
933 return output
934 else:
935 raise Exception("status is not completed: {}".format(status))
936 except Exception as e:
937 self.log.error(
938 "Error executing primitive {}: {}".format(primitive_name, e)
939 )
940 raise N2VCExecutionException(
941 message="Error executing primitive {} into ee={} : {}".format(
942 primitive_name, ee_id, e
943 ),
944 primitive_name=primitive_name,
945 )
946
947 async def disconnect(self):
948 self.log.info("closing juju N2VC...")
949 try:
950 await self.libjuju.disconnect()
951 except Exception as e:
952 raise N2VCConnectionException(
953 message="Error disconnecting controller: {}".format(e), url=self.url
954 )
955
956 """
957 ####################################################################################
958 ################################### P R I V A T E ##################################
959 ####################################################################################
960 """
961
962 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
963
964 # write ee_id to database: _admin.deployed.VCA.x
965 try:
966 the_table = db_dict["collection"]
967 the_filter = db_dict["filter"]
968 the_path = db_dict["path"]
969 if not the_path[-1] == ".":
970 the_path = the_path + "."
971 update_dict = {the_path + "ee_id": ee_id}
972 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
973 self.db.set_one(
974 table=the_table,
975 q_filter=the_filter,
976 update_dict=update_dict,
977 fail_on_empty=True,
978 )
979 except asyncio.CancelledError:
980 raise
981 except Exception as e:
982 self.log.error("Error writing ee_id to database: {}".format(e))
983
984 @staticmethod
985 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
986 """
987 Build an execution environment id form model, application and machine
988 :param model_name:
989 :param application_name:
990 :param machine_id:
991 :return:
992 """
993 # id for the execution environment
994 return "{}.{}.{}".format(model_name, application_name, machine_id)
995
996 @staticmethod
997 def _get_ee_id_components(ee_id: str) -> (str, str, str):
998 """
999 Get model, application and machine components from an execution environment id
1000 :param ee_id:
1001 :return: model_name, application_name, machine_id
1002 """
1003
1004 if ee_id is None:
1005 return None, None, None
1006
1007 # split components of id
1008 parts = ee_id.split(".")
1009 model_name = parts[0]
1010 application_name = parts[1]
1011 machine_id = parts[2]
1012 return model_name, application_name, machine_id
1013
1014 def _get_application_name(self, namespace: str) -> str:
1015 """
1016 Build application name from namespace
1017 :param namespace:
1018 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1019 """
1020
1021 # TODO: Enforce the Juju 50-character application limit
1022
1023 # split namespace components
1024 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1025 namespace=namespace
1026 )
1027
1028 if vnf_id is None or len(vnf_id) == 0:
1029 vnf_id = ""
1030 else:
1031 # Shorten the vnf_id to its last twelve characters
1032 vnf_id = "vnf-" + vnf_id[-12:]
1033
1034 if vdu_id is None or len(vdu_id) == 0:
1035 vdu_id = ""
1036 else:
1037 # Shorten the vdu_id to its last twelve characters
1038 vdu_id = "-vdu-" + vdu_id[-12:]
1039
1040 if vdu_count is None or len(vdu_count) == 0:
1041 vdu_count = ""
1042 else:
1043 vdu_count = "-cnt-" + vdu_count
1044
1045 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1046
1047 return N2VCJujuConnector._format_app_name(application_name)
1048
1049 def _create_juju_public_key(self):
1050 """Recreate the Juju public key on lcm container, if needed
1051 Certain libjuju commands expect to be run from the same machine as Juju
1052 is bootstrapped to. This method will write the public key to disk in
1053 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1054 """
1055
1056 # Make sure that we have a public key before writing to disk
1057 if self.public_key is None or len(self.public_key) == 0:
1058 if "OSMLCM_VCA_PUBKEY" in os.environ:
1059 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1060 if len(self.public_key) == 0:
1061 return
1062 else:
1063 return
1064
1065 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1066 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1067 self.log.debug(
1068 "writing juju public key to file:\n{}\npublic key: {}".format(
1069 file_path, self.public_key
1070 )
1071 )
1072 if not os.path.exists(pk_path):
1073 # create path and write file
1074 os.makedirs(pk_path)
1075 with open(file_path, "w") as f:
1076 self.log.debug("Creating juju public key file: {}".format(file_path))
1077 f.write(self.public_key)
1078 else:
1079 self.log.debug("juju public key file already exists: {}".format(file_path))
1080
1081 @staticmethod
1082 def _format_model_name(name: str) -> str:
1083 """Format the name of the model.
1084
1085 Model names may only contain lowercase letters, digits and hyphens
1086 """
1087
1088 return name.replace("_", "-").replace(" ", "-").lower()
1089
1090 @staticmethod
1091 def _format_app_name(name: str) -> str:
1092 """Format the name of the application (in order to assure valid application name).
1093
1094 Application names have restrictions (run juju deploy --help):
1095 - contains lowercase letters 'a'-'z'
1096 - contains numbers '0'-'9'
1097 - contains hyphens '-'
1098 - starts with a lowercase letter
1099 - not two or more consecutive hyphens
1100 - after a hyphen, not a group with all numbers
1101 """
1102
1103 def all_numbers(s: str) -> bool:
1104 for c in s:
1105 if not c.isdigit():
1106 return False
1107 return True
1108
1109 new_name = name.replace("_", "-")
1110 new_name = new_name.replace(" ", "-")
1111 new_name = new_name.lower()
1112 while new_name.find("--") >= 0:
1113 new_name = new_name.replace("--", "-")
1114 groups = new_name.split("-")
1115
1116 # find 'all numbers' groups and prefix them with a letter
1117 app_name = ""
1118 for i in range(len(groups)):
1119 group = groups[i]
1120 if all_numbers(group):
1121 group = "z" + group
1122 if i > 0:
1123 app_name += "-"
1124 app_name += group
1125
1126 if app_name[0].isdigit():
1127 app_name = "z" + app_name
1128
1129 return app_name