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