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