Pin chardet to 3.0.4
[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 if not await self.libjuju.model_exists(model_name):
547 cloud = cloud_name or self.k8s_cloud
548 credential = credential_name or cloud_name if cloud_name else self.k8s_cloud
549 await self.libjuju.add_model(
550 model_name,
551 cloud_name=cloud,
552 credential_name=credential
553 )
554 application_name = self._get_application_name(namespace)
555
556 try:
557 await self.libjuju.deploy_charm(
558 model_name=model_name,
559 application_name=application_name,
560 path=full_path,
561 machine_id=None,
562 db_dict=db_dict,
563 progress_timeout=progress_timeout,
564 total_timeout=total_timeout,
565 config=config
566 )
567 except Exception as e:
568 raise N2VCException(message='Error deploying charm: {}'.format(e))
569
570 self.log.info('K8s proxy charm installed')
571 ee_id = N2VCJujuConnector._build_ee_id(
572 model_name=model_name,
573 application_name=application_name,
574 machine_id="k8s",
575 )
576
577 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
578
579 return ee_id
580
581 async def get_ee_ssh_public__key(
582 self,
583 ee_id: str,
584 db_dict: dict,
585 progress_timeout: float = None,
586 total_timeout: float = None,
587 ) -> str:
588
589 self.log.info(
590 (
591 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
592 ).format(ee_id, db_dict)
593 )
594
595 # check arguments
596 if ee_id is None or len(ee_id) == 0:
597 raise N2VCBadArgumentsException(
598 message="ee_id is mandatory", bad_args=["ee_id"]
599 )
600 if db_dict is None:
601 raise N2VCBadArgumentsException(
602 message="db_dict is mandatory", bad_args=["db_dict"]
603 )
604
605 try:
606 (
607 model_name,
608 application_name,
609 machine_id,
610 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
611 self.log.debug(
612 "model: {}, application: {}, machine: {}".format(
613 model_name, application_name, machine_id
614 )
615 )
616 except Exception:
617 raise N2VCBadArgumentsException(
618 message="ee_id={} is not a valid execution environment id".format(
619 ee_id
620 ),
621 bad_args=["ee_id"],
622 )
623
624 # try to execute ssh layer primitives (if exist):
625 # generate-ssh-key
626 # get-ssh-public-key
627
628 output = None
629
630 application_name = N2VCJujuConnector._format_app_name(application_name)
631
632 # execute action: generate-ssh-key
633 try:
634 output, _status = await self.libjuju.execute_action(
635 model_name=model_name,
636 application_name=application_name,
637 action_name="generate-ssh-key",
638 db_dict=db_dict,
639 progress_timeout=progress_timeout,
640 total_timeout=total_timeout,
641 )
642 except Exception as e:
643 self.log.info(
644 "Skipping exception while executing action generate-ssh-key: {}".format(
645 e
646 )
647 )
648
649 # execute action: get-ssh-public-key
650 try:
651 output, _status = await self.libjuju.execute_action(
652 model_name=model_name,
653 application_name=application_name,
654 action_name="get-ssh-public-key",
655 db_dict=db_dict,
656 progress_timeout=progress_timeout,
657 total_timeout=total_timeout,
658 )
659 except Exception as e:
660 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
661 self.log.info(msg)
662 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
663
664 # return public key if exists
665 return output["pubkey"] if "pubkey" in output else output
666
667 async def get_metrics(self, model_name: str, application_name: str) -> dict:
668 return await self.libjuju.get_metrics(model_name, application_name)
669
670 async def add_relation(
671 self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
672 ):
673
674 self.log.debug(
675 "adding new relation between {} and {}, endpoints: {}, {}".format(
676 ee_id_1, ee_id_2, endpoint_1, endpoint_2
677 )
678 )
679
680 # check arguments
681 if not ee_id_1:
682 message = "EE 1 is mandatory"
683 self.log.error(message)
684 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
685 if not ee_id_2:
686 message = "EE 2 is mandatory"
687 self.log.error(message)
688 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
689 if not endpoint_1:
690 message = "endpoint 1 is mandatory"
691 self.log.error(message)
692 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
693 if not endpoint_2:
694 message = "endpoint 2 is mandatory"
695 self.log.error(message)
696 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
697
698 # get the model, the applications and the machines from the ee_id's
699 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
700 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
701
702 # model must be the same
703 if model_1 != model_2:
704 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
705 self.log.error(message)
706 raise N2VCBadArgumentsException(
707 message=message, bad_args=["ee_id_1", "ee_id_2"]
708 )
709
710 # add juju relations between two applications
711 try:
712 await self.libjuju.add_relation(
713 model_name=model_1,
714 endpoint_1="{}:{}".format(app_1, endpoint_1),
715 endpoint_2="{}:{}".format(app_2, endpoint_2),
716 )
717 except Exception as e:
718 message = "Error adding relation between {} and {}: {}".format(
719 ee_id_1, ee_id_2, e
720 )
721 self.log.error(message)
722 raise N2VCException(message=message)
723
724 async def remove_relation(self):
725 # TODO
726 self.log.info("Method not implemented yet")
727 raise MethodNotImplemented()
728
729 async def deregister_execution_environments(self):
730 self.log.info("Method not implemented yet")
731 raise MethodNotImplemented()
732
733 async def delete_namespace(
734 self, namespace: str, db_dict: dict = None, total_timeout: float = None
735 ):
736 self.log.info("Deleting namespace={}".format(namespace))
737
738 # check arguments
739 if namespace is None:
740 raise N2VCBadArgumentsException(
741 message="namespace is mandatory", bad_args=["namespace"]
742 )
743
744 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
745 namespace=namespace
746 )
747 if ns_id is not None:
748 try:
749 models = await self.libjuju.list_models(contains=ns_id)
750 for model in models:
751 await self.libjuju.destroy_model(
752 model_name=model, total_timeout=total_timeout
753 )
754 except Exception as e:
755 raise N2VCException(
756 message="Error deleting namespace {} : {}".format(namespace, e)
757 )
758 else:
759 raise N2VCBadArgumentsException(
760 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
761 )
762
763 self.log.info("Namespace {} deleted".format(namespace))
764
765 async def delete_execution_environment(
766 self, ee_id: str, db_dict: dict = None, total_timeout: float = None,
767 scaling_in: bool = False
768 ):
769 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
770
771 # check arguments
772 if ee_id is None:
773 raise N2VCBadArgumentsException(
774 message="ee_id is mandatory", bad_args=["ee_id"]
775 )
776
777 model_name, application_name, _machine_id = self._get_ee_id_components(
778 ee_id=ee_id
779 )
780 try:
781 if not scaling_in:
782 # destroy the model
783 # TODO: should this be removed?
784 await self.libjuju.destroy_model(
785 model_name=model_name, total_timeout=total_timeout
786 )
787 else:
788 # get juju model and observer
789 controller = await self.libjuju.get_controller()
790 model = await self.libjuju.get_model(controller, model_name)
791 # destroy the application
792 await self.libjuju.destroy_application(
793 model=model, application_name=application_name)
794 except Exception as e:
795 raise N2VCException(
796 message=(
797 "Error deleting execution environment {} (application {}) : {}"
798 ).format(ee_id, application_name, e)
799 )
800
801 self.log.info("Execution environment {} deleted".format(ee_id))
802
803 async def exec_primitive(
804 self,
805 ee_id: str,
806 primitive_name: str,
807 params_dict: dict,
808 db_dict: dict = None,
809 progress_timeout: float = None,
810 total_timeout: float = None,
811 ) -> str:
812
813 self.log.info(
814 "Executing primitive: {} on ee: {}, params: {}".format(
815 primitive_name, ee_id, params_dict
816 )
817 )
818
819 # check arguments
820 if ee_id is None or len(ee_id) == 0:
821 raise N2VCBadArgumentsException(
822 message="ee_id is mandatory", bad_args=["ee_id"]
823 )
824 if primitive_name is None or len(primitive_name) == 0:
825 raise N2VCBadArgumentsException(
826 message="action_name is mandatory", bad_args=["action_name"]
827 )
828 if params_dict is None:
829 params_dict = dict()
830
831 try:
832 (
833 model_name,
834 application_name,
835 _machine_id,
836 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
837 except Exception:
838 raise N2VCBadArgumentsException(
839 message="ee_id={} is not a valid execution environment id".format(
840 ee_id
841 ),
842 bad_args=["ee_id"],
843 )
844
845 if primitive_name == "config":
846 # Special case: config primitive
847 try:
848 await self.libjuju.configure_application(
849 model_name=model_name,
850 application_name=application_name,
851 config=params_dict,
852 )
853 actions = await self.libjuju.get_actions(
854 application_name=application_name, model_name=model_name,
855 )
856 self.log.debug(
857 "Application {} has these actions: {}".format(
858 application_name, actions
859 )
860 )
861 if "verify-ssh-credentials" in actions:
862 # execute verify-credentials
863 num_retries = 20
864 retry_timeout = 15.0
865 for _ in range(num_retries):
866 try:
867 self.log.debug("Executing action verify-ssh-credentials...")
868 output, ok = await self.libjuju.execute_action(
869 model_name=model_name,
870 application_name=application_name,
871 action_name="verify-ssh-credentials",
872 db_dict=db_dict,
873 progress_timeout=progress_timeout,
874 total_timeout=total_timeout,
875 )
876
877 if ok == "failed":
878 self.log.debug(
879 "Error executing verify-ssh-credentials: {}. Retrying..."
880 )
881 await asyncio.sleep(retry_timeout)
882
883 continue
884 self.log.debug("Result: {}, output: {}".format(ok, output))
885 break
886 except asyncio.CancelledError:
887 raise
888 else:
889 self.log.error(
890 "Error executing verify-ssh-credentials after {} retries. ".format(
891 num_retries
892 )
893 )
894 else:
895 msg = "Action verify-ssh-credentials does not exist in application {}".format(
896 application_name
897 )
898 self.log.debug(msg=msg)
899 except Exception as e:
900 self.log.error("Error configuring juju application: {}".format(e))
901 raise N2VCExecutionException(
902 message="Error configuring application into ee={} : {}".format(
903 ee_id, e
904 ),
905 primitive_name=primitive_name,
906 )
907 return "CONFIG OK"
908 else:
909 try:
910 output, status = await self.libjuju.execute_action(
911 model_name=model_name,
912 application_name=application_name,
913 action_name=primitive_name,
914 db_dict=db_dict,
915 progress_timeout=progress_timeout,
916 total_timeout=total_timeout,
917 **params_dict
918 )
919 if status == "completed":
920 return output
921 else:
922 raise Exception("status is not completed: {}".format(status))
923 except Exception as e:
924 self.log.error(
925 "Error executing primitive {}: {}".format(primitive_name, e)
926 )
927 raise N2VCExecutionException(
928 message="Error executing primitive {} into ee={} : {}".format(
929 primitive_name, ee_id, e
930 ),
931 primitive_name=primitive_name,
932 )
933
934 async def disconnect(self):
935 self.log.info("closing juju N2VC...")
936 try:
937 await self.libjuju.disconnect()
938 except Exception as e:
939 raise N2VCConnectionException(
940 message="Error disconnecting controller: {}".format(e), url=self.url
941 )
942
943 """
944 ####################################################################################
945 ################################### P R I V A T E ##################################
946 ####################################################################################
947 """
948
949 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
950
951 # write ee_id to database: _admin.deployed.VCA.x
952 try:
953 the_table = db_dict["collection"]
954 the_filter = db_dict["filter"]
955 the_path = db_dict["path"]
956 if not the_path[-1] == ".":
957 the_path = the_path + "."
958 update_dict = {the_path + "ee_id": ee_id}
959 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
960 self.db.set_one(
961 table=the_table,
962 q_filter=the_filter,
963 update_dict=update_dict,
964 fail_on_empty=True,
965 )
966 except asyncio.CancelledError:
967 raise
968 except Exception as e:
969 self.log.error("Error writing ee_id to database: {}".format(e))
970
971 @staticmethod
972 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
973 """
974 Build an execution environment id form model, application and machine
975 :param model_name:
976 :param application_name:
977 :param machine_id:
978 :return:
979 """
980 # id for the execution environment
981 return "{}.{}.{}".format(model_name, application_name, machine_id)
982
983 @staticmethod
984 def _get_ee_id_components(ee_id: str) -> (str, str, str):
985 """
986 Get model, application and machine components from an execution environment id
987 :param ee_id:
988 :return: model_name, application_name, machine_id
989 """
990
991 if ee_id is None:
992 return None, None, None
993
994 # split components of id
995 parts = ee_id.split(".")
996 model_name = parts[0]
997 application_name = parts[1]
998 machine_id = parts[2]
999 return model_name, application_name, machine_id
1000
1001 def _get_application_name(self, namespace: str) -> str:
1002 """
1003 Build application name from namespace
1004 :param namespace:
1005 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1006 """
1007
1008 # TODO: Enforce the Juju 50-character application limit
1009
1010 # split namespace components
1011 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1012 namespace=namespace
1013 )
1014
1015 if vnf_id is None or len(vnf_id) == 0:
1016 vnf_id = ""
1017 else:
1018 # Shorten the vnf_id to its last twelve characters
1019 vnf_id = "vnf-" + vnf_id[-12:]
1020
1021 if vdu_id is None or len(vdu_id) == 0:
1022 vdu_id = ""
1023 else:
1024 # Shorten the vdu_id to its last twelve characters
1025 vdu_id = "-vdu-" + vdu_id[-12:]
1026
1027 if vdu_count is None or len(vdu_count) == 0:
1028 vdu_count = ""
1029 else:
1030 vdu_count = "-cnt-" + vdu_count
1031
1032 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1033
1034 return N2VCJujuConnector._format_app_name(application_name)
1035
1036 def _create_juju_public_key(self):
1037 """Recreate the Juju public key on lcm container, if needed
1038 Certain libjuju commands expect to be run from the same machine as Juju
1039 is bootstrapped to. This method will write the public key to disk in
1040 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1041 """
1042
1043 # Make sure that we have a public key before writing to disk
1044 if self.public_key is None or len(self.public_key) == 0:
1045 if "OSMLCM_VCA_PUBKEY" in os.environ:
1046 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1047 if len(self.public_key) == 0:
1048 return
1049 else:
1050 return
1051
1052 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1053 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1054 self.log.debug(
1055 "writing juju public key to file:\n{}\npublic key: {}".format(
1056 file_path, self.public_key
1057 )
1058 )
1059 if not os.path.exists(pk_path):
1060 # create path and write file
1061 os.makedirs(pk_path)
1062 with open(file_path, "w") as f:
1063 self.log.debug("Creating juju public key file: {}".format(file_path))
1064 f.write(self.public_key)
1065 else:
1066 self.log.debug("juju public key file already exists: {}".format(file_path))
1067
1068 @staticmethod
1069 def _format_model_name(name: str) -> str:
1070 """Format the name of the model.
1071
1072 Model names may only contain lowercase letters, digits and hyphens
1073 """
1074
1075 return name.replace("_", "-").replace(" ", "-").lower()
1076
1077 @staticmethod
1078 def _format_app_name(name: str) -> str:
1079 """Format the name of the application (in order to assure valid application name).
1080
1081 Application names have restrictions (run juju deploy --help):
1082 - contains lowercase letters 'a'-'z'
1083 - contains numbers '0'-'9'
1084 - contains hyphens '-'
1085 - starts with a lowercase letter
1086 - not two or more consecutive hyphens
1087 - after a hyphen, not a group with all numbers
1088 """
1089
1090 def all_numbers(s: str) -> bool:
1091 for c in s:
1092 if not c.isdigit():
1093 return False
1094 return True
1095
1096 new_name = name.replace("_", "-")
1097 new_name = new_name.replace(" ", "-")
1098 new_name = new_name.lower()
1099 while new_name.find("--") >= 0:
1100 new_name = new_name.replace("--", "-")
1101 groups = new_name.split("-")
1102
1103 # find 'all numbers' groups and prefix them with a letter
1104 app_name = ""
1105 for i in range(len(groups)):
1106 group = groups[i]
1107 if all_numbers(group):
1108 group = "z" + group
1109 if i > 0:
1110 app_name += "-"
1111 app_name += group
1112
1113 if app_name[0].isdigit():
1114 app_name = "z" + app_name
1115
1116 return app_name