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