Fix bug 1263
[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 ) -> (str, dict):
232
233 self.log.info(
234 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
235 namespace, reuse_ee_id
236 )
237 )
238
239 machine_id = None
240 if reuse_ee_id:
241 model_name, application_name, machine_id = self._get_ee_id_components(
242 ee_id=reuse_ee_id
243 )
244 else:
245 (
246 _nsi_id,
247 ns_id,
248 _vnf_id,
249 _vdu_id,
250 _vdu_count,
251 ) = self._get_namespace_components(namespace=namespace)
252 # model name is ns_id
253 model_name = ns_id
254 # application name
255 application_name = self._get_application_name(namespace=namespace)
256
257 self.log.debug(
258 "model name: {}, application name: {}, machine_id: {}".format(
259 model_name, application_name, machine_id
260 )
261 )
262
263 # create or reuse a new juju machine
264 try:
265 if not await self.libjuju.model_exists(model_name):
266 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
267 machine, new = await self.libjuju.create_machine(
268 model_name=model_name,
269 machine_id=machine_id,
270 db_dict=db_dict,
271 progress_timeout=progress_timeout,
272 total_timeout=total_timeout,
273 )
274 # id for the execution environment
275 ee_id = N2VCJujuConnector._build_ee_id(
276 model_name=model_name,
277 application_name=application_name,
278 machine_id=str(machine.entity_id),
279 )
280 self.log.debug("ee_id: {}".format(ee_id))
281
282 if new:
283 # write ee_id in database
284 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
285
286 except Exception as e:
287 message = "Error creating machine on juju: {}".format(e)
288 self.log.error(message)
289 raise N2VCException(message=message)
290
291 # new machine credentials
292 credentials = {
293 "hostname": machine.dns_name,
294 }
295
296 self.log.info(
297 "Execution environment created. ee_id: {}, credentials: {}".format(
298 ee_id, credentials
299 )
300 )
301
302 return ee_id, credentials
303
304 async def register_execution_environment(
305 self,
306 namespace: str,
307 credentials: dict,
308 db_dict: dict,
309 progress_timeout: float = None,
310 total_timeout: float = None,
311 ) -> str:
312
313 self.log.info(
314 "Registering execution environment. namespace={}, credentials={}".format(
315 namespace, credentials
316 )
317 )
318
319 if credentials is None:
320 raise N2VCBadArgumentsException(
321 message="credentials are mandatory", bad_args=["credentials"]
322 )
323 if credentials.get("hostname"):
324 hostname = credentials["hostname"]
325 else:
326 raise N2VCBadArgumentsException(
327 message="hostname is mandatory", bad_args=["credentials.hostname"]
328 )
329 if credentials.get("username"):
330 username = credentials["username"]
331 else:
332 raise N2VCBadArgumentsException(
333 message="username is mandatory", bad_args=["credentials.username"]
334 )
335 if "private_key_path" in credentials:
336 private_key_path = credentials["private_key_path"]
337 else:
338 # if not passed as argument, use generated private key path
339 private_key_path = self.private_key_path
340
341 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
342 namespace=namespace
343 )
344
345 # model name
346 model_name = ns_id
347 # application name
348 application_name = self._get_application_name(namespace=namespace)
349
350 # register machine on juju
351 try:
352 if not await self.libjuju.model_exists(model_name):
353 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
354 machine_id = await self.libjuju.provision_machine(
355 model_name=model_name,
356 hostname=hostname,
357 username=username,
358 private_key_path=private_key_path,
359 db_dict=db_dict,
360 progress_timeout=progress_timeout,
361 total_timeout=total_timeout,
362 )
363 except Exception as e:
364 self.log.error("Error registering machine: {}".format(e))
365 raise N2VCException(
366 message="Error registering machine on juju: {}".format(e)
367 )
368
369 self.log.info("Machine registered: {}".format(machine_id))
370
371 # id for the execution environment
372 ee_id = N2VCJujuConnector._build_ee_id(
373 model_name=model_name,
374 application_name=application_name,
375 machine_id=str(machine_id),
376 )
377
378 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
379
380 return ee_id
381
382 async def install_configuration_sw(
383 self,
384 ee_id: str,
385 artifact_path: str,
386 db_dict: dict,
387 progress_timeout: float = None,
388 total_timeout: float = None,
389 config: dict = None,
390 num_units: int = 1,
391 ):
392
393 self.log.info(
394 (
395 "Installing configuration sw on ee_id: {}, "
396 "artifact path: {}, db_dict: {}"
397 ).format(ee_id, artifact_path, db_dict)
398 )
399
400 # check arguments
401 if ee_id is None or len(ee_id) == 0:
402 raise N2VCBadArgumentsException(
403 message="ee_id is mandatory", bad_args=["ee_id"]
404 )
405 if artifact_path is None or len(artifact_path) == 0:
406 raise N2VCBadArgumentsException(
407 message="artifact_path is mandatory", bad_args=["artifact_path"]
408 )
409 if db_dict is None:
410 raise N2VCBadArgumentsException(
411 message="db_dict is mandatory", bad_args=["db_dict"]
412 )
413
414 try:
415 (
416 model_name,
417 application_name,
418 machine_id,
419 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
420 self.log.debug(
421 "model: {}, application: {}, machine: {}".format(
422 model_name, application_name, machine_id
423 )
424 )
425 except Exception:
426 raise N2VCBadArgumentsException(
427 message="ee_id={} is not a valid execution environment id".format(
428 ee_id
429 ),
430 bad_args=["ee_id"],
431 )
432
433 # remove // in charm path
434 while artifact_path.find("//") >= 0:
435 artifact_path = artifact_path.replace("//", "/")
436
437 # check charm path
438 if not self.fs.file_exists(artifact_path, mode="dir"):
439 msg = "artifact path does not exist: {}".format(artifact_path)
440 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
441
442 if artifact_path.startswith("/"):
443 full_path = self.fs.path + artifact_path
444 else:
445 full_path = self.fs.path + "/" + artifact_path
446
447 try:
448 await self.libjuju.deploy_charm(
449 model_name=model_name,
450 application_name=application_name,
451 path=full_path,
452 machine_id=machine_id,
453 db_dict=db_dict,
454 progress_timeout=progress_timeout,
455 total_timeout=total_timeout,
456 config=config,
457 num_units=num_units,
458 )
459 except Exception as e:
460 raise N2VCException(
461 message="Error desploying charm into ee={} : {}".format(ee_id, e)
462 )
463
464 self.log.info("Configuration sw installed")
465
466 async def install_k8s_proxy_charm(
467 self,
468 charm_name: str,
469 namespace: str,
470 artifact_path: str,
471 db_dict: dict,
472 progress_timeout: float = None,
473 total_timeout: float = None,
474 config: dict = None,
475 ) -> str:
476 """
477 Install a k8s proxy charm
478
479 :param charm_name: Name of the charm being deployed
480 :param namespace: collection of all the uuids related to the charm.
481 :param str artifact_path: where to locate the artifacts (parent folder) using
482 the self.fs
483 the final artifact path will be a combination of this artifact_path and
484 additional string from the config_dict (e.g. charm name)
485 :param dict db_dict: where to write into database when the status changes.
486 It contains a dict with
487 {collection: <str>, filter: {}, path: <str>},
488 e.g. {collection: "nsrs", filter:
489 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
490 :param float progress_timeout:
491 :param float total_timeout:
492 :param config: Dictionary with additional configuration
493
494 :returns ee_id: execution environment id.
495 """
496 self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
497 .format(charm_name, artifact_path, db_dict))
498
499 if not self.k8s_cloud:
500 raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
501
502 if artifact_path is None or len(artifact_path) == 0:
503 raise N2VCBadArgumentsException(
504 message="artifact_path is mandatory", bad_args=["artifact_path"]
505 )
506 if db_dict is None:
507 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
508
509 # remove // in charm path
510 while artifact_path.find('//') >= 0:
511 artifact_path = artifact_path.replace('//', '/')
512
513 # check charm path
514 if not self.fs.file_exists(artifact_path, mode="dir"):
515 msg = 'artifact path does not exist: {}'.format(artifact_path)
516 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
517
518 if artifact_path.startswith('/'):
519 full_path = self.fs.path + artifact_path
520 else:
521 full_path = self.fs.path + '/' + artifact_path
522
523 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
524 model_name = '{}-k8s'.format(ns_id)
525
526 await self.libjuju.add_model(model_name, self.k8s_cloud)
527 application_name = self._get_application_name(namespace)
528
529 try:
530 await self.libjuju.deploy_charm(
531 model_name=model_name,
532 application_name=application_name,
533 path=full_path,
534 machine_id=None,
535 db_dict=db_dict,
536 progress_timeout=progress_timeout,
537 total_timeout=total_timeout,
538 config=config
539 )
540 except Exception as e:
541 raise N2VCException(message='Error deploying charm: {}'.format(e))
542
543 self.log.info('K8s proxy charm installed')
544 ee_id = N2VCJujuConnector._build_ee_id(
545 model_name=model_name,
546 application_name=application_name,
547 machine_id="k8s",
548 )
549
550 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
551
552 return ee_id
553
554 async def get_ee_ssh_public__key(
555 self,
556 ee_id: str,
557 db_dict: dict,
558 progress_timeout: float = None,
559 total_timeout: float = None,
560 ) -> str:
561
562 self.log.info(
563 (
564 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
565 ).format(ee_id, db_dict)
566 )
567
568 # check arguments
569 if ee_id is None or len(ee_id) == 0:
570 raise N2VCBadArgumentsException(
571 message="ee_id is mandatory", bad_args=["ee_id"]
572 )
573 if db_dict is None:
574 raise N2VCBadArgumentsException(
575 message="db_dict is mandatory", bad_args=["db_dict"]
576 )
577
578 try:
579 (
580 model_name,
581 application_name,
582 machine_id,
583 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
584 self.log.debug(
585 "model: {}, application: {}, machine: {}".format(
586 model_name, application_name, machine_id
587 )
588 )
589 except Exception:
590 raise N2VCBadArgumentsException(
591 message="ee_id={} is not a valid execution environment id".format(
592 ee_id
593 ),
594 bad_args=["ee_id"],
595 )
596
597 # try to execute ssh layer primitives (if exist):
598 # generate-ssh-key
599 # get-ssh-public-key
600
601 output = None
602
603 application_name = N2VCJujuConnector._format_app_name(application_name)
604
605 # execute action: generate-ssh-key
606 try:
607 output, _status = await self.libjuju.execute_action(
608 model_name=model_name,
609 application_name=application_name,
610 action_name="generate-ssh-key",
611 db_dict=db_dict,
612 progress_timeout=progress_timeout,
613 total_timeout=total_timeout,
614 )
615 except Exception as e:
616 self.log.info(
617 "Skipping exception while executing action generate-ssh-key: {}".format(
618 e
619 )
620 )
621
622 # execute action: get-ssh-public-key
623 try:
624 output, _status = await self.libjuju.execute_action(
625 model_name=model_name,
626 application_name=application_name,
627 action_name="get-ssh-public-key",
628 db_dict=db_dict,
629 progress_timeout=progress_timeout,
630 total_timeout=total_timeout,
631 )
632 except Exception as e:
633 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
634 self.log.info(msg)
635 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
636
637 # return public key if exists
638 return output["pubkey"] if "pubkey" in output else output
639
640 async def get_metrics(self, model_name: str, application_name: str) -> dict:
641 return await self.libjuju.get_metrics(model_name, application_name)
642
643 async def add_relation(
644 self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
645 ):
646
647 self.log.debug(
648 "adding new relation between {} and {}, endpoints: {}, {}".format(
649 ee_id_1, ee_id_2, endpoint_1, endpoint_2
650 )
651 )
652
653 # check arguments
654 if not ee_id_1:
655 message = "EE 1 is mandatory"
656 self.log.error(message)
657 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
658 if not ee_id_2:
659 message = "EE 2 is mandatory"
660 self.log.error(message)
661 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
662 if not endpoint_1:
663 message = "endpoint 1 is mandatory"
664 self.log.error(message)
665 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
666 if not endpoint_2:
667 message = "endpoint 2 is mandatory"
668 self.log.error(message)
669 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
670
671 # get the model, the applications and the machines from the ee_id's
672 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
673 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
674
675 # model must be the same
676 if model_1 != model_2:
677 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
678 self.log.error(message)
679 raise N2VCBadArgumentsException(
680 message=message, bad_args=["ee_id_1", "ee_id_2"]
681 )
682
683 # add juju relations between two applications
684 try:
685 await self.libjuju.add_relation(
686 model_name=model_1,
687 endpoint_1="{}:{}".format(app_1, endpoint_1),
688 endpoint_2="{}:{}".format(app_2, endpoint_2),
689 )
690 except Exception as e:
691 message = "Error adding relation between {} and {}: {}".format(
692 ee_id_1, ee_id_2, e
693 )
694 self.log.error(message)
695 raise N2VCException(message=message)
696
697 async def remove_relation(self):
698 # TODO
699 self.log.info("Method not implemented yet")
700 raise MethodNotImplemented()
701
702 async def deregister_execution_environments(self):
703 self.log.info("Method not implemented yet")
704 raise MethodNotImplemented()
705
706 async def delete_namespace(
707 self, namespace: str, db_dict: dict = None, total_timeout: float = None
708 ):
709 self.log.info("Deleting namespace={}".format(namespace))
710
711 # check arguments
712 if namespace is None:
713 raise N2VCBadArgumentsException(
714 message="namespace is mandatory", bad_args=["namespace"]
715 )
716
717 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
718 namespace=namespace
719 )
720 if ns_id is not None:
721 try:
722 models = await self.libjuju.list_models(contains=ns_id)
723 for model in models:
724 await self.libjuju.destroy_model(
725 model_name=model, total_timeout=total_timeout
726 )
727 except Exception as e:
728 raise N2VCException(
729 message="Error deleting namespace {} : {}".format(namespace, e)
730 )
731 else:
732 raise N2VCBadArgumentsException(
733 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
734 )
735
736 self.log.info("Namespace {} deleted".format(namespace))
737
738 async def delete_execution_environment(
739 self, ee_id: str, db_dict: dict = None, total_timeout: float = None
740 ):
741 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
742
743 # check arguments
744 if ee_id is None:
745 raise N2VCBadArgumentsException(
746 message="ee_id is mandatory", bad_args=["ee_id"]
747 )
748
749 model_name, application_name, _machine_id = self._get_ee_id_components(
750 ee_id=ee_id
751 )
752
753 # destroy the application
754 try:
755 await self.libjuju.destroy_model(
756 model_name=model_name, total_timeout=total_timeout
757 )
758 except Exception as e:
759 raise N2VCException(
760 message=(
761 "Error deleting execution environment {} (application {}) : {}"
762 ).format(ee_id, application_name, e)
763 )
764
765 # destroy the machine
766 # try:
767 # await self._juju_destroy_machine(
768 # model_name=model_name,
769 # machine_id=machine_id,
770 # total_timeout=total_timeout
771 # )
772 # except Exception as e:
773 # raise N2VCException(
774 # message='Error deleting execution environment {} (machine {}) : {}'
775 # .format(ee_id, machine_id, e))
776
777 self.log.info("Execution environment {} deleted".format(ee_id))
778
779 async def exec_primitive(
780 self,
781 ee_id: str,
782 primitive_name: str,
783 params_dict: dict,
784 db_dict: dict = None,
785 progress_timeout: float = None,
786 total_timeout: float = None,
787 ) -> str:
788
789 self.log.info(
790 "Executing primitive: {} on ee: {}, params: {}".format(
791 primitive_name, ee_id, params_dict
792 )
793 )
794
795 # check arguments
796 if ee_id is None or len(ee_id) == 0:
797 raise N2VCBadArgumentsException(
798 message="ee_id is mandatory", bad_args=["ee_id"]
799 )
800 if primitive_name is None or len(primitive_name) == 0:
801 raise N2VCBadArgumentsException(
802 message="action_name is mandatory", bad_args=["action_name"]
803 )
804 if params_dict is None:
805 params_dict = dict()
806
807 try:
808 (
809 model_name,
810 application_name,
811 _machine_id,
812 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
813 except Exception:
814 raise N2VCBadArgumentsException(
815 message="ee_id={} is not a valid execution environment id".format(
816 ee_id
817 ),
818 bad_args=["ee_id"],
819 )
820
821 if primitive_name == "config":
822 # Special case: config primitive
823 try:
824 await self.libjuju.configure_application(
825 model_name=model_name,
826 application_name=application_name,
827 config=params_dict,
828 )
829 actions = await self.libjuju.get_actions(
830 application_name=application_name, model_name=model_name,
831 )
832 self.log.debug(
833 "Application {} has these actions: {}".format(
834 application_name, actions
835 )
836 )
837 if "verify-ssh-credentials" in actions:
838 # execute verify-credentials
839 num_retries = 20
840 retry_timeout = 15.0
841 for _ in range(num_retries):
842 try:
843 self.log.debug("Executing action verify-ssh-credentials...")
844 output, ok = await self.libjuju.execute_action(
845 model_name=model_name,
846 application_name=application_name,
847 action_name="verify-ssh-credentials",
848 db_dict=db_dict,
849 progress_timeout=progress_timeout,
850 total_timeout=total_timeout,
851 )
852
853 if ok == "failed":
854 self.log.debug(
855 "Error executing verify-ssh-credentials: {}. Retrying..."
856 )
857 await asyncio.sleep(retry_timeout)
858
859 continue
860 self.log.debug("Result: {}, output: {}".format(ok, output))
861 break
862 except asyncio.CancelledError:
863 raise
864 else:
865 self.log.error(
866 "Error executing verify-ssh-credentials after {} retries. ".format(
867 num_retries
868 )
869 )
870 else:
871 msg = "Action verify-ssh-credentials does not exist in application {}".format(
872 application_name
873 )
874 self.log.debug(msg=msg)
875 except Exception as e:
876 self.log.error("Error configuring juju application: {}".format(e))
877 raise N2VCExecutionException(
878 message="Error configuring application into ee={} : {}".format(
879 ee_id, e
880 ),
881 primitive_name=primitive_name,
882 )
883 return "CONFIG OK"
884 else:
885 try:
886 output, status = await self.libjuju.execute_action(
887 model_name=model_name,
888 application_name=application_name,
889 action_name=primitive_name,
890 db_dict=db_dict,
891 progress_timeout=progress_timeout,
892 total_timeout=total_timeout,
893 **params_dict
894 )
895 if status == "completed":
896 return output
897 else:
898 raise Exception("status is not completed: {}".format(status))
899 except Exception as e:
900 self.log.error(
901 "Error executing primitive {}: {}".format(primitive_name, e)
902 )
903 raise N2VCExecutionException(
904 message="Error executing primitive {} into ee={} : {}".format(
905 primitive_name, ee_id, e
906 ),
907 primitive_name=primitive_name,
908 )
909
910 async def disconnect(self):
911 self.log.info("closing juju N2VC...")
912 try:
913 await self.libjuju.disconnect()
914 except Exception as e:
915 raise N2VCConnectionException(
916 message="Error disconnecting controller: {}".format(e), url=self.url
917 )
918
919 """
920 ####################################################################################
921 ################################### P R I V A T E ##################################
922 ####################################################################################
923 """
924
925 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
926
927 # write ee_id to database: _admin.deployed.VCA.x
928 try:
929 the_table = db_dict["collection"]
930 the_filter = db_dict["filter"]
931 the_path = db_dict["path"]
932 if not the_path[-1] == ".":
933 the_path = the_path + "."
934 update_dict = {the_path + "ee_id": ee_id}
935 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
936 self.db.set_one(
937 table=the_table,
938 q_filter=the_filter,
939 update_dict=update_dict,
940 fail_on_empty=True,
941 )
942 except asyncio.CancelledError:
943 raise
944 except Exception as e:
945 self.log.error("Error writing ee_id to database: {}".format(e))
946
947 @staticmethod
948 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
949 """
950 Build an execution environment id form model, application and machine
951 :param model_name:
952 :param application_name:
953 :param machine_id:
954 :return:
955 """
956 # id for the execution environment
957 return "{}.{}.{}".format(model_name, application_name, machine_id)
958
959 @staticmethod
960 def _get_ee_id_components(ee_id: str) -> (str, str, str):
961 """
962 Get model, application and machine components from an execution environment id
963 :param ee_id:
964 :return: model_name, application_name, machine_id
965 """
966
967 if ee_id is None:
968 return None, None, None
969
970 # split components of id
971 parts = ee_id.split(".")
972 model_name = parts[0]
973 application_name = parts[1]
974 machine_id = parts[2]
975 return model_name, application_name, machine_id
976
977 def _get_application_name(self, namespace: str) -> str:
978 """
979 Build application name from namespace
980 :param namespace:
981 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
982 """
983
984 # TODO: Enforce the Juju 50-character application limit
985
986 # split namespace components
987 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
988 namespace=namespace
989 )
990
991 if vnf_id is None or len(vnf_id) == 0:
992 vnf_id = ""
993 else:
994 # Shorten the vnf_id to its last twelve characters
995 vnf_id = "vnf-" + vnf_id[-12:]
996
997 if vdu_id is None or len(vdu_id) == 0:
998 vdu_id = ""
999 else:
1000 # Shorten the vdu_id to its last twelve characters
1001 vdu_id = "-vdu-" + vdu_id[-12:]
1002
1003 if vdu_count is None or len(vdu_count) == 0:
1004 vdu_count = ""
1005 else:
1006 vdu_count = "-cnt-" + vdu_count
1007
1008 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1009
1010 return N2VCJujuConnector._format_app_name(application_name)
1011
1012 def _create_juju_public_key(self):
1013 """Recreate the Juju public key on lcm container, if needed
1014 Certain libjuju commands expect to be run from the same machine as Juju
1015 is bootstrapped to. This method will write the public key to disk in
1016 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1017 """
1018
1019 # Make sure that we have a public key before writing to disk
1020 if self.public_key is None or len(self.public_key) == 0:
1021 if "OSMLCM_VCA_PUBKEY" in os.environ:
1022 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1023 if len(self.public_key) == 0:
1024 return
1025 else:
1026 return
1027
1028 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1029 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1030 self.log.debug(
1031 "writing juju public key to file:\n{}\npublic key: {}".format(
1032 file_path, self.public_key
1033 )
1034 )
1035 if not os.path.exists(pk_path):
1036 # create path and write file
1037 os.makedirs(pk_path)
1038 with open(file_path, "w") as f:
1039 self.log.debug("Creating juju public key file: {}".format(file_path))
1040 f.write(self.public_key)
1041 else:
1042 self.log.debug("juju public key file already exists: {}".format(file_path))
1043
1044 @staticmethod
1045 def _format_model_name(name: str) -> str:
1046 """Format the name of the model.
1047
1048 Model names may only contain lowercase letters, digits and hyphens
1049 """
1050
1051 return name.replace("_", "-").replace(" ", "-").lower()
1052
1053 @staticmethod
1054 def _format_app_name(name: str) -> str:
1055 """Format the name of the application (in order to assure valid application name).
1056
1057 Application names have restrictions (run juju deploy --help):
1058 - contains lowercase letters 'a'-'z'
1059 - contains numbers '0'-'9'
1060 - contains hyphens '-'
1061 - starts with a lowercase letter
1062 - not two or more consecutive hyphens
1063 - after a hyphen, not a group with all numbers
1064 """
1065
1066 def all_numbers(s: str) -> bool:
1067 for c in s:
1068 if not c.isdigit():
1069 return False
1070 return True
1071
1072 new_name = name.replace("_", "-")
1073 new_name = new_name.replace(" ", "-")
1074 new_name = new_name.lower()
1075 while new_name.find("--") >= 0:
1076 new_name = new_name.replace("--", "-")
1077 groups = new_name.split("-")
1078
1079 # find 'all numbers' groups and prefix them with a letter
1080 app_name = ""
1081 for i in range(len(groups)):
1082 group = groups[i]
1083 if all_numbers(group):
1084 group = "z" + group
1085 if i > 0:
1086 app_name += "-"
1087 app_name += group
1088
1089 if app_name[0].isdigit():
1090 app_name = "z" + app_name
1091
1092 return app_name