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