Bug 1938 fixed: : added a random suffix to the end of the Juju app name, in order...
[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
26 from n2vc.config import EnvironConfig
27 from n2vc.exceptions import (
28 N2VCBadArgumentsException,
29 N2VCException,
30 N2VCConnectionException,
31 N2VCExecutionException,
32 N2VCApplicationExists,
33 JujuApplicationExists,
34 # N2VCNotFound,
35 MethodNotImplemented,
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.store import MotorStore
41 from n2vc.utils import generate_random_alfanum_string
42 from n2vc.vca.connection import get_connection
43 from retrying_async import retry
44
45
46 class N2VCJujuConnector(N2VCConnector):
47
48 """
49 ####################################################################################
50 ################################### P U B L I C ####################################
51 ####################################################################################
52 """
53
54 BUILT_IN_CLOUDS = ["localhost", "microk8s"]
55 libjuju = None
56
57 def __init__(
58 self,
59 db: object,
60 fs: object,
61 log: object = None,
62 loop: object = None,
63 on_update_db=None,
64 ):
65 """
66 Constructor
67
68 :param: db: Database object from osm_common
69 :param: fs: Filesystem object from osm_common
70 :param: log: Logger
71 :param: loop: Asyncio loop
72 :param: on_update_db: Callback function to be called for updating the database.
73 """
74
75 # parent class constructor
76 N2VCConnector.__init__(
77 self,
78 db=db,
79 fs=fs,
80 log=log,
81 loop=loop,
82 on_update_db=on_update_db,
83 )
84
85 # silence websocket traffic log
86 logging.getLogger("websockets.protocol").setLevel(logging.INFO)
87 logging.getLogger("juju.client.connection").setLevel(logging.WARN)
88 logging.getLogger("model").setLevel(logging.WARN)
89
90 self.log.info("Initializing N2VC juju connector...")
91
92 db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
93 self._store = MotorStore(db_uri)
94 self.loading_libjuju = asyncio.Lock(loop=self.loop)
95
96 self.log.info("N2VC juju connector initialized")
97
98 async def get_status(
99 self, namespace: str, yaml_format: bool = True, vca_id: str = None
100 ):
101 """
102 Get status from all juju models from a VCA
103
104 :param namespace: we obtain ns from namespace
105 :param yaml_format: returns a yaml string
106 :param: vca_id: VCA ID from which the status will be retrieved.
107 """
108 # TODO: Review where is this function used. It is not optimal at all to get the status
109 # from all the juju models of a particular VCA. Additionally, these models might
110 # not have been deployed by OSM, in that case we are getting information from
111 # deployments outside of OSM's scope.
112
113 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
114 libjuju = await self._get_libjuju(vca_id)
115
116 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
117 namespace=namespace
118 )
119 # model name is ns_id
120 model_name = ns_id
121 if model_name is None:
122 msg = "Namespace {} not valid".format(namespace)
123 self.log.error(msg)
124 raise N2VCBadArgumentsException(msg, ["namespace"])
125
126 status = {}
127 models = await libjuju.list_models(contains=ns_id)
128
129 for m in models:
130 status[m] = await libjuju.get_model_status(m)
131
132 if yaml_format:
133 return obj_to_yaml(status)
134 else:
135 return obj_to_dict(status)
136
137 async def update_vca_status(self, vcastatus: dict, vca_id: str = None):
138 """
139 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
140
141 :param vcastatus: dict containing vcaStatus
142 :param: vca_id: VCA ID
143
144 :return: None
145 """
146 try:
147 libjuju = await self._get_libjuju(vca_id)
148 for model_name in vcastatus:
149 # Adding executed actions
150 vcastatus[model_name][
151 "executedActions"
152 ] = await libjuju.get_executed_actions(model_name)
153 for application in vcastatus[model_name]["applications"]:
154 # Adding application actions
155 vcastatus[model_name]["applications"][application][
156 "actions"
157 ] = await libjuju.get_actions(application, model_name)
158 # Adding application configs
159 vcastatus[model_name]["applications"][application][
160 "configs"
161 ] = await libjuju.get_application_configs(model_name, application)
162 except Exception as e:
163 self.log.debug("Error in updating vca status: {}".format(str(e)))
164
165 async def create_execution_environment(
166 self,
167 namespace: str,
168 db_dict: dict,
169 reuse_ee_id: str = None,
170 progress_timeout: float = None,
171 total_timeout: float = None,
172 vca_id: str = None,
173 ) -> (str, dict):
174 """
175 Create an Execution Environment. Returns when it is created or raises an
176 exception on failing
177
178 :param: namespace: Contains a dot separate string.
179 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
180 :param: db_dict: where to write to database when the status changes.
181 It contains a dictionary with {collection: str, filter: {}, path: str},
182 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
183 "_admin.deployed.VCA.3"}
184 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
185 older environment
186 :param: progress_timeout: Progress timeout
187 :param: total_timeout: Total timeout
188 :param: vca_id: VCA ID
189
190 :returns: id of the new execution environment and credentials for it
191 (credentials can contains hostname, username, etc depending on underlying cloud)
192 """
193
194 self.log.info(
195 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
196 namespace, reuse_ee_id
197 )
198 )
199 libjuju = await self._get_libjuju(vca_id)
200
201 machine_id = None
202 if reuse_ee_id:
203 model_name, application_name, machine_id = self._get_ee_id_components(
204 ee_id=reuse_ee_id
205 )
206 else:
207 (
208 _nsi_id,
209 ns_id,
210 _vnf_id,
211 _vdu_id,
212 _vdu_count,
213 ) = self._get_namespace_components(namespace=namespace)
214 # model name is ns_id
215 model_name = ns_id
216 # application name
217 application_name = self._get_application_name(namespace=namespace)
218
219 self.log.debug(
220 "model name: {}, application name: {}, machine_id: {}".format(
221 model_name, application_name, machine_id
222 )
223 )
224
225 # create or reuse a new juju machine
226 try:
227 if not await libjuju.model_exists(model_name):
228 await libjuju.add_model(
229 model_name,
230 libjuju.vca_connection.lxd_cloud,
231 )
232 machine, new = await libjuju.create_machine(
233 model_name=model_name,
234 machine_id=machine_id,
235 db_dict=db_dict,
236 progress_timeout=progress_timeout,
237 total_timeout=total_timeout,
238 )
239 # id for the execution environment
240 ee_id = N2VCJujuConnector._build_ee_id(
241 model_name=model_name,
242 application_name=application_name,
243 machine_id=str(machine.entity_id),
244 )
245 self.log.debug("ee_id: {}".format(ee_id))
246
247 if new:
248 # write ee_id in database
249 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
250
251 except Exception as e:
252 message = "Error creating machine on juju: {}".format(e)
253 self.log.error(message)
254 raise N2VCException(message=message)
255
256 # new machine credentials
257 credentials = {
258 "hostname": machine.dns_name,
259 }
260
261 self.log.info(
262 "Execution environment created. ee_id: {}, credentials: {}".format(
263 ee_id, credentials
264 )
265 )
266
267 return ee_id, credentials
268
269 async def register_execution_environment(
270 self,
271 namespace: str,
272 credentials: dict,
273 db_dict: dict,
274 progress_timeout: float = None,
275 total_timeout: float = None,
276 vca_id: str = None,
277 ) -> str:
278 """
279 Register an existing execution environment at the VCA
280
281 :param: namespace: Contains a dot separate string.
282 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
283 :param: credentials: credentials to access the existing execution environment
284 (it can contains hostname, username, path to private key,
285 etc depending on underlying cloud)
286 :param: db_dict: where to write to database when the status changes.
287 It contains a dictionary with {collection: str, filter: {}, path: str},
288 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
289 "_admin.deployed.VCA.3"}
290 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
291 older environment
292 :param: progress_timeout: Progress timeout
293 :param: total_timeout: Total timeout
294 :param: vca_id: VCA ID
295
296 :returns: id of the execution environment
297 """
298 self.log.info(
299 "Registering execution environment. namespace={}, credentials={}".format(
300 namespace, credentials
301 )
302 )
303 libjuju = await self._get_libjuju(vca_id)
304
305 if credentials is None:
306 raise N2VCBadArgumentsException(
307 message="credentials are mandatory", bad_args=["credentials"]
308 )
309 if credentials.get("hostname"):
310 hostname = credentials["hostname"]
311 else:
312 raise N2VCBadArgumentsException(
313 message="hostname is mandatory", bad_args=["credentials.hostname"]
314 )
315 if credentials.get("username"):
316 username = credentials["username"]
317 else:
318 raise N2VCBadArgumentsException(
319 message="username is mandatory", bad_args=["credentials.username"]
320 )
321 if "private_key_path" in credentials:
322 private_key_path = credentials["private_key_path"]
323 else:
324 # if not passed as argument, use generated private key path
325 private_key_path = self.private_key_path
326
327 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
328 namespace=namespace
329 )
330
331 # model name
332 model_name = ns_id
333 # application name
334 application_name = self._get_application_name(namespace=namespace)
335
336 # register machine on juju
337 try:
338 if not await libjuju.model_exists(model_name):
339 await libjuju.add_model(
340 model_name,
341 libjuju.vca_connection.lxd_cloud,
342 )
343 machine_id = await libjuju.provision_machine(
344 model_name=model_name,
345 hostname=hostname,
346 username=username,
347 private_key_path=private_key_path,
348 db_dict=db_dict,
349 progress_timeout=progress_timeout,
350 total_timeout=total_timeout,
351 )
352 except Exception as e:
353 self.log.error("Error registering machine: {}".format(e))
354 raise N2VCException(
355 message="Error registering machine on juju: {}".format(e)
356 )
357
358 self.log.info("Machine registered: {}".format(machine_id))
359
360 # id for the execution environment
361 ee_id = N2VCJujuConnector._build_ee_id(
362 model_name=model_name,
363 application_name=application_name,
364 machine_id=str(machine_id),
365 )
366
367 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
368
369 return ee_id
370
371 # In case of native_charm is being deployed, if JujuApplicationExists error happens
372 # it will try to add_unit
373 @retry(attempts=3, delay=5, retry_exceptions=(N2VCApplicationExists,), timeout=None)
374 async def install_configuration_sw(
375 self,
376 ee_id: str,
377 artifact_path: str,
378 db_dict: dict,
379 progress_timeout: float = None,
380 total_timeout: float = None,
381 config: dict = None,
382 num_units: int = 1,
383 vca_id: str = None,
384 scaling_out: bool = False,
385 vca_type: str = None,
386 ):
387 """
388 Install the software inside the execution environment identified by ee_id
389
390 :param: ee_id: the id of the execution environment returned by
391 create_execution_environment or register_execution_environment
392 :param: artifact_path: where to locate the artifacts (parent folder) using
393 the self.fs
394 the final artifact path will be a combination of this
395 artifact_path and additional string from the config_dict
396 (e.g. charm name)
397 :param: db_dict: where to write into database when the status changes.
398 It contains a dict with
399 {collection: <str>, filter: {}, path: <str>},
400 e.g. {collection: "nsrs", filter:
401 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
402 :param: progress_timeout: Progress timeout
403 :param: total_timeout: Total timeout
404 :param: config: Dictionary with deployment config information.
405 :param: num_units: Number of units to deploy of a particular charm.
406 :param: vca_id: VCA ID
407 :param: scaling_out: Boolean to indicate if it is a scaling out operation
408 :param: vca_type: VCA type
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 libjuju = await self._get_libjuju(vca_id)
418
419 # check arguments
420 if ee_id is None or len(ee_id) == 0:
421 raise N2VCBadArgumentsException(
422 message="ee_id is mandatory", bad_args=["ee_id"]
423 )
424 if artifact_path is None or len(artifact_path) == 0:
425 raise N2VCBadArgumentsException(
426 message="artifact_path is mandatory", bad_args=["artifact_path"]
427 )
428 if db_dict is None:
429 raise N2VCBadArgumentsException(
430 message="db_dict is mandatory", bad_args=["db_dict"]
431 )
432
433 try:
434 (
435 model_name,
436 application_name,
437 machine_id,
438 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
439 self.log.debug(
440 "model: {}, application: {}, machine: {}".format(
441 model_name, application_name, machine_id
442 )
443 )
444 except Exception:
445 raise N2VCBadArgumentsException(
446 message="ee_id={} is not a valid execution environment id".format(
447 ee_id
448 ),
449 bad_args=["ee_id"],
450 )
451
452 # remove // in charm path
453 while artifact_path.find("//") >= 0:
454 artifact_path = artifact_path.replace("//", "/")
455
456 # check charm path
457 if not self.fs.file_exists(artifact_path):
458 msg = "artifact path does not exist: {}".format(artifact_path)
459 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
460
461 if artifact_path.startswith("/"):
462 full_path = self.fs.path + artifact_path
463 else:
464 full_path = self.fs.path + "/" + artifact_path
465
466 try:
467 if vca_type == "native_charm" and await libjuju.check_application_exists(
468 model_name, application_name
469 ):
470 await libjuju.add_unit(
471 application_name=application_name,
472 model_name=model_name,
473 machine_id=machine_id,
474 db_dict=db_dict,
475 progress_timeout=progress_timeout,
476 total_timeout=total_timeout,
477 )
478 else:
479 await libjuju.deploy_charm(
480 model_name=model_name,
481 application_name=application_name,
482 path=full_path,
483 machine_id=machine_id,
484 db_dict=db_dict,
485 progress_timeout=progress_timeout,
486 total_timeout=total_timeout,
487 config=config,
488 num_units=num_units,
489 )
490 except JujuApplicationExists as e:
491 raise N2VCApplicationExists(
492 message="Error deploying charm into ee={} : {}".format(ee_id, e.message)
493 )
494 except Exception as e:
495 raise N2VCException(
496 message="Error deploying charm into ee={} : {}".format(ee_id, e)
497 )
498
499 self.log.info("Configuration sw installed")
500
501 async def install_k8s_proxy_charm(
502 self,
503 charm_name: str,
504 namespace: str,
505 artifact_path: str,
506 db_dict: dict,
507 progress_timeout: float = None,
508 total_timeout: float = None,
509 config: dict = None,
510 vca_id: str = None,
511 ) -> str:
512 """
513 Install a k8s proxy charm
514
515 :param charm_name: Name of the charm being deployed
516 :param namespace: collection of all the uuids related to the charm.
517 :param str artifact_path: where to locate the artifacts (parent folder) using
518 the self.fs
519 the final artifact path will be a combination of this artifact_path and
520 additional string from the config_dict (e.g. charm name)
521 :param dict db_dict: where to write into database when the status changes.
522 It contains a dict with
523 {collection: <str>, filter: {}, path: <str>},
524 e.g. {collection: "nsrs", filter:
525 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
526 :param: progress_timeout: Progress timeout
527 :param: total_timeout: Total timeout
528 :param config: Dictionary with additional configuration
529 :param vca_id: VCA ID
530
531 :returns ee_id: execution environment id.
532 """
533 self.log.info(
534 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
535 charm_name, artifact_path, db_dict
536 )
537 )
538 libjuju = await self._get_libjuju(vca_id)
539
540 if artifact_path is None or len(artifact_path) == 0:
541 raise N2VCBadArgumentsException(
542 message="artifact_path is mandatory", bad_args=["artifact_path"]
543 )
544 if db_dict is None:
545 raise N2VCBadArgumentsException(
546 message="db_dict is mandatory", bad_args=["db_dict"]
547 )
548
549 # remove // in charm path
550 while artifact_path.find("//") >= 0:
551 artifact_path = artifact_path.replace("//", "/")
552
553 # check charm path
554 if not self.fs.file_exists(artifact_path):
555 msg = "artifact path does not exist: {}".format(artifact_path)
556 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
557
558 if artifact_path.startswith("/"):
559 full_path = self.fs.path + artifact_path
560 else:
561 full_path = self.fs.path + "/" + artifact_path
562
563 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
564 model_name = "{}-k8s".format(ns_id)
565 if not await libjuju.model_exists(model_name):
566 await libjuju.add_model(
567 model_name,
568 libjuju.vca_connection.k8s_cloud,
569 )
570 application_name = self._get_application_name(namespace)
571
572 try:
573 await libjuju.deploy_charm(
574 model_name=model_name,
575 application_name=application_name,
576 path=full_path,
577 machine_id=None,
578 db_dict=db_dict,
579 progress_timeout=progress_timeout,
580 total_timeout=total_timeout,
581 config=config,
582 )
583 except Exception as e:
584 raise N2VCException(message="Error deploying charm: {}".format(e))
585
586 self.log.info("K8s proxy charm installed")
587 ee_id = N2VCJujuConnector._build_ee_id(
588 model_name=model_name,
589 application_name=application_name,
590 machine_id="k8s",
591 )
592
593 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
594
595 return ee_id
596
597 async def get_ee_ssh_public__key(
598 self,
599 ee_id: str,
600 db_dict: dict,
601 progress_timeout: float = None,
602 total_timeout: float = None,
603 vca_id: str = None,
604 ) -> str:
605 """
606 Get Execution environment ssh public key
607
608 :param: ee_id: the id of the execution environment returned by
609 create_execution_environment or register_execution_environment
610 :param: db_dict: where to write into database when the status changes.
611 It contains a dict with
612 {collection: <str>, filter: {}, path: <str>},
613 e.g. {collection: "nsrs", filter:
614 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
615 :param: progress_timeout: Progress timeout
616 :param: total_timeout: Total timeout
617 :param vca_id: VCA ID
618 :returns: public key of the execution environment
619 For the case of juju proxy charm ssh-layered, it is the one
620 returned by 'get-ssh-public-key' primitive.
621 It raises a N2VC exception if fails
622 """
623
624 self.log.info(
625 (
626 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
627 ).format(ee_id, db_dict)
628 )
629 libjuju = await self._get_libjuju(vca_id)
630
631 # check arguments
632 if ee_id is None or len(ee_id) == 0:
633 raise N2VCBadArgumentsException(
634 message="ee_id is mandatory", bad_args=["ee_id"]
635 )
636 if db_dict is None:
637 raise N2VCBadArgumentsException(
638 message="db_dict is mandatory", bad_args=["db_dict"]
639 )
640
641 try:
642 (
643 model_name,
644 application_name,
645 machine_id,
646 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
647 self.log.debug(
648 "model: {}, application: {}, machine: {}".format(
649 model_name, application_name, machine_id
650 )
651 )
652 except Exception:
653 raise N2VCBadArgumentsException(
654 message="ee_id={} is not a valid execution environment id".format(
655 ee_id
656 ),
657 bad_args=["ee_id"],
658 )
659
660 # try to execute ssh layer primitives (if exist):
661 # generate-ssh-key
662 # get-ssh-public-key
663
664 output = None
665
666 application_name = N2VCJujuConnector._format_app_name(application_name)
667
668 # execute action: generate-ssh-key
669 try:
670 output, _status = await libjuju.execute_action(
671 model_name=model_name,
672 application_name=application_name,
673 action_name="generate-ssh-key",
674 db_dict=db_dict,
675 progress_timeout=progress_timeout,
676 total_timeout=total_timeout,
677 )
678 except Exception as e:
679 self.log.info(
680 "Skipping exception while executing action generate-ssh-key: {}".format(
681 e
682 )
683 )
684
685 # execute action: get-ssh-public-key
686 try:
687 output, _status = await libjuju.execute_action(
688 model_name=model_name,
689 application_name=application_name,
690 action_name="get-ssh-public-key",
691 db_dict=db_dict,
692 progress_timeout=progress_timeout,
693 total_timeout=total_timeout,
694 )
695 except Exception as e:
696 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
697 self.log.info(msg)
698 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
699
700 # return public key if exists
701 return output["pubkey"] if "pubkey" in output else output
702
703 async def get_metrics(
704 self, model_name: str, application_name: str, vca_id: str = None
705 ) -> dict:
706 """
707 Get metrics from application
708
709 :param: model_name: Model name
710 :param: application_name: Application name
711 :param: vca_id: VCA ID
712
713 :return: Dictionary with obtained metrics
714 """
715 libjuju = await self._get_libjuju(vca_id)
716 return await libjuju.get_metrics(model_name, application_name)
717
718 async def add_relation(
719 self,
720 ee_id_1: str,
721 ee_id_2: str,
722 endpoint_1: str,
723 endpoint_2: str,
724 vca_id: str = None,
725 ):
726 """
727 Add relation between two charmed endpoints
728
729 :param: ee_id_1: The id of the first execution environment
730 :param: ee_id_2: The id of the second execution environment
731 :param: endpoint_1: The endpoint in the first execution environment
732 :param: endpoint_2: The endpoint in the second execution environment
733 :param: vca_id: VCA ID
734 """
735 self.log.debug(
736 "adding new relation between {} and {}, endpoints: {}, {}".format(
737 ee_id_1, ee_id_2, endpoint_1, endpoint_2
738 )
739 )
740 libjuju = await self._get_libjuju(vca_id)
741
742 # check arguments
743 if not ee_id_1:
744 message = "EE 1 is mandatory"
745 self.log.error(message)
746 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
747 if not ee_id_2:
748 message = "EE 2 is mandatory"
749 self.log.error(message)
750 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
751 if not endpoint_1:
752 message = "endpoint 1 is mandatory"
753 self.log.error(message)
754 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
755 if not endpoint_2:
756 message = "endpoint 2 is mandatory"
757 self.log.error(message)
758 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
759
760 # get the model, the applications and the machines from the ee_id's
761 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
762 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
763
764 # model must be the same
765 if model_1 != model_2:
766 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
767 self.log.error(message)
768 raise N2VCBadArgumentsException(
769 message=message, bad_args=["ee_id_1", "ee_id_2"]
770 )
771
772 # add juju relations between two applications
773 try:
774 await libjuju.add_relation(
775 model_name=model_1,
776 endpoint_1="{}:{}".format(app_1, endpoint_1),
777 endpoint_2="{}:{}".format(app_2, endpoint_2),
778 )
779 except Exception as e:
780 message = "Error adding relation between {} and {}: {}".format(
781 ee_id_1, ee_id_2, e
782 )
783 self.log.error(message)
784 raise N2VCException(message=message)
785
786 async def remove_relation(self):
787 # TODO
788 self.log.info("Method not implemented yet")
789 raise MethodNotImplemented()
790
791 async def deregister_execution_environments(self):
792 self.log.info("Method not implemented yet")
793 raise MethodNotImplemented()
794
795 async def delete_namespace(
796 self,
797 namespace: str,
798 db_dict: dict = None,
799 total_timeout: float = None,
800 vca_id: str = None,
801 ):
802 """
803 Remove a network scenario and its execution environments
804 :param: namespace: [<nsi-id>].<ns-id>
805 :param: db_dict: where to write into database when the status changes.
806 It contains a dict with
807 {collection: <str>, filter: {}, path: <str>},
808 e.g. {collection: "nsrs", filter:
809 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
810 :param: total_timeout: Total timeout
811 :param: vca_id: VCA ID
812 """
813 self.log.info("Deleting namespace={}".format(namespace))
814 libjuju = await self._get_libjuju(vca_id)
815
816 # check arguments
817 if namespace is None:
818 raise N2VCBadArgumentsException(
819 message="namespace is mandatory", bad_args=["namespace"]
820 )
821
822 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
823 namespace=namespace
824 )
825 if ns_id is not None:
826 try:
827 models = await libjuju.list_models(contains=ns_id)
828 for model in models:
829 await libjuju.destroy_model(
830 model_name=model, total_timeout=total_timeout
831 )
832 except Exception as e:
833 raise N2VCException(
834 message="Error deleting namespace {} : {}".format(namespace, e)
835 )
836 else:
837 raise N2VCBadArgumentsException(
838 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
839 )
840
841 self.log.info("Namespace {} deleted".format(namespace))
842
843 async def delete_execution_environment(
844 self,
845 ee_id: str,
846 db_dict: dict = None,
847 total_timeout: float = None,
848 scaling_in: bool = False,
849 vca_type: str = None,
850 vca_id: str = None,
851 ):
852 """
853 Delete an execution environment
854 :param str ee_id: id of the execution environment to delete
855 :param dict db_dict: where to write into database when the status changes.
856 It contains a dict with
857 {collection: <str>, filter: {}, path: <str>},
858 e.g. {collection: "nsrs", filter:
859 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
860 :param: total_timeout: Total timeout
861 :param: scaling_in: Boolean to indicate if it is a scaling in operation
862 :param: vca_type: VCA type
863 :param: vca_id: VCA ID
864 """
865 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
866 libjuju = await self._get_libjuju(vca_id)
867
868 # check arguments
869 if ee_id is None:
870 raise N2VCBadArgumentsException(
871 message="ee_id is mandatory", bad_args=["ee_id"]
872 )
873
874 model_name, application_name, machine_id = self._get_ee_id_components(
875 ee_id=ee_id
876 )
877 try:
878 if not scaling_in:
879 # destroy the model
880 await libjuju.destroy_model(
881 model_name=model_name,
882 total_timeout=total_timeout,
883 )
884 elif vca_type == "native_charm" and scaling_in:
885 # destroy the unit in the application
886 await libjuju.destroy_unit(
887 application_name=application_name,
888 model_name=model_name,
889 machine_id=machine_id,
890 total_timeout=total_timeout,
891 )
892 else:
893 # destroy the application
894 await libjuju.destroy_application(
895 model_name=model_name,
896 application_name=application_name,
897 total_timeout=total_timeout,
898 )
899 except Exception as e:
900 raise N2VCException(
901 message=(
902 "Error deleting execution environment {} (application {}) : {}"
903 ).format(ee_id, application_name, e)
904 )
905
906 self.log.info("Execution environment {} deleted".format(ee_id))
907
908 async def exec_primitive(
909 self,
910 ee_id: str,
911 primitive_name: str,
912 params_dict: dict,
913 db_dict: dict = None,
914 progress_timeout: float = None,
915 total_timeout: float = None,
916 vca_id: str = None,
917 vca_type: str = None,
918 ) -> str:
919 """
920 Execute a primitive in the execution environment
921
922 :param: ee_id: the one returned by create_execution_environment or
923 register_execution_environment
924 :param: primitive_name: must be one defined in the software. There is one
925 called 'config', where, for the proxy case, the 'credentials' of VM are
926 provided
927 :param: params_dict: parameters of the action
928 :param: db_dict: where to write into database when the status changes.
929 It contains a dict with
930 {collection: <str>, filter: {}, path: <str>},
931 e.g. {collection: "nsrs", filter:
932 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
933 :param: progress_timeout: Progress timeout
934 :param: total_timeout: Total timeout
935 :param: vca_id: VCA ID
936 :param: vca_type: VCA type
937 :returns str: primitive result, if ok. It raises exceptions in case of fail
938 """
939
940 self.log.info(
941 "Executing primitive: {} on ee: {}, params: {}".format(
942 primitive_name, ee_id, params_dict
943 )
944 )
945 libjuju = await self._get_libjuju(vca_id)
946
947 # check arguments
948 if ee_id is None or len(ee_id) == 0:
949 raise N2VCBadArgumentsException(
950 message="ee_id is mandatory", bad_args=["ee_id"]
951 )
952 if primitive_name is None or len(primitive_name) == 0:
953 raise N2VCBadArgumentsException(
954 message="action_name is mandatory", bad_args=["action_name"]
955 )
956 if params_dict is None:
957 params_dict = dict()
958
959 try:
960 (
961 model_name,
962 application_name,
963 machine_id,
964 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
965 # To run action on the leader unit in libjuju.execute_action function,
966 # machine_id must be set to None if vca_type is not native_charm
967 if vca_type != "native_charm":
968 machine_id = None
969 except Exception:
970 raise N2VCBadArgumentsException(
971 message="ee_id={} is not a valid execution environment id".format(
972 ee_id
973 ),
974 bad_args=["ee_id"],
975 )
976
977 if primitive_name == "config":
978 # Special case: config primitive
979 try:
980 await libjuju.configure_application(
981 model_name=model_name,
982 application_name=application_name,
983 config=params_dict,
984 )
985 actions = await libjuju.get_actions(
986 application_name=application_name,
987 model_name=model_name,
988 )
989 self.log.debug(
990 "Application {} has these actions: {}".format(
991 application_name, actions
992 )
993 )
994 if "verify-ssh-credentials" in actions:
995 # execute verify-credentials
996 num_retries = 20
997 retry_timeout = 15.0
998 for _ in range(num_retries):
999 try:
1000 self.log.debug("Executing action verify-ssh-credentials...")
1001 output, ok = await libjuju.execute_action(
1002 model_name=model_name,
1003 application_name=application_name,
1004 action_name="verify-ssh-credentials",
1005 db_dict=db_dict,
1006 progress_timeout=progress_timeout,
1007 total_timeout=total_timeout,
1008 )
1009
1010 if ok == "failed":
1011 self.log.debug(
1012 "Error executing verify-ssh-credentials: {}. Retrying..."
1013 )
1014 await asyncio.sleep(retry_timeout)
1015
1016 continue
1017 self.log.debug("Result: {}, output: {}".format(ok, output))
1018 break
1019 except asyncio.CancelledError:
1020 raise
1021 else:
1022 self.log.error(
1023 "Error executing verify-ssh-credentials after {} retries. ".format(
1024 num_retries
1025 )
1026 )
1027 else:
1028 msg = "Action verify-ssh-credentials does not exist in application {}".format(
1029 application_name
1030 )
1031 self.log.debug(msg=msg)
1032 except Exception as e:
1033 self.log.error("Error configuring juju application: {}".format(e))
1034 raise N2VCExecutionException(
1035 message="Error configuring application into ee={} : {}".format(
1036 ee_id, e
1037 ),
1038 primitive_name=primitive_name,
1039 )
1040 return "CONFIG OK"
1041 else:
1042 try:
1043 output, status = await libjuju.execute_action(
1044 model_name=model_name,
1045 application_name=application_name,
1046 action_name=primitive_name,
1047 db_dict=db_dict,
1048 machine_id=machine_id,
1049 progress_timeout=progress_timeout,
1050 total_timeout=total_timeout,
1051 **params_dict
1052 )
1053 if status == "completed":
1054 return output
1055 else:
1056 raise Exception("status is not completed: {}".format(status))
1057 except Exception as e:
1058 self.log.error(
1059 "Error executing primitive {}: {}".format(primitive_name, e)
1060 )
1061 raise N2VCExecutionException(
1062 message="Error executing primitive {} into ee={} : {}".format(
1063 primitive_name, ee_id, e
1064 ),
1065 primitive_name=primitive_name,
1066 )
1067
1068 async def disconnect(self, vca_id: str = None):
1069 """
1070 Disconnect from VCA
1071
1072 :param: vca_id: VCA ID
1073 """
1074 self.log.info("closing juju N2VC...")
1075 libjuju = await self._get_libjuju(vca_id)
1076 try:
1077 await libjuju.disconnect()
1078 except Exception as e:
1079 raise N2VCConnectionException(
1080 message="Error disconnecting controller: {}".format(e),
1081 url=libjuju.vca_connection.data.endpoints,
1082 )
1083
1084 """
1085 ####################################################################################
1086 ################################### P R I V A T E ##################################
1087 ####################################################################################
1088 """
1089
1090 async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
1091 """
1092 Get libjuju object
1093
1094 :param: vca_id: VCA ID
1095 If None, get a libjuju object with a Connection to the default VCA
1096 Else, geta libjuju object with a Connection to the specified VCA
1097 """
1098 if not vca_id:
1099 while self.loading_libjuju.locked():
1100 await asyncio.sleep(0.1)
1101 if not self.libjuju:
1102 async with self.loading_libjuju:
1103 vca_connection = await get_connection(self._store)
1104 self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
1105 return self.libjuju
1106 else:
1107 vca_connection = await get_connection(self._store, vca_id)
1108 return Libjuju(
1109 vca_connection,
1110 loop=self.loop,
1111 log=self.log,
1112 n2vc=self,
1113 )
1114
1115 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
1116
1117 # write ee_id to database: _admin.deployed.VCA.x
1118 try:
1119 the_table = db_dict["collection"]
1120 the_filter = db_dict["filter"]
1121 the_path = db_dict["path"]
1122 if not the_path[-1] == ".":
1123 the_path = the_path + "."
1124 update_dict = {the_path + "ee_id": ee_id}
1125 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1126 self.db.set_one(
1127 table=the_table,
1128 q_filter=the_filter,
1129 update_dict=update_dict,
1130 fail_on_empty=True,
1131 )
1132 except asyncio.CancelledError:
1133 raise
1134 except Exception as e:
1135 self.log.error("Error writing ee_id to database: {}".format(e))
1136
1137 @staticmethod
1138 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
1139 """
1140 Build an execution environment id form model, application and machine
1141 :param model_name:
1142 :param application_name:
1143 :param machine_id:
1144 :return:
1145 """
1146 # id for the execution environment
1147 return "{}.{}.{}".format(model_name, application_name, machine_id)
1148
1149 @staticmethod
1150 def _get_ee_id_components(ee_id: str) -> (str, str, str):
1151 """
1152 Get model, application and machine components from an execution environment id
1153 :param ee_id:
1154 :return: model_name, application_name, machine_id
1155 """
1156
1157 if ee_id is None:
1158 return None, None, None
1159
1160 # split components of id
1161 parts = ee_id.split(".")
1162 model_name = parts[0]
1163 application_name = parts[1]
1164 machine_id = parts[2]
1165 return model_name, application_name, machine_id
1166
1167 def _get_application_name(self, namespace: str) -> str:
1168 """
1169 Build application name from namespace
1170 :param namespace:
1171 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
1172 """
1173
1174 # TODO: Enforce the Juju 50-character application limit
1175
1176 # split namespace components
1177 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1178 namespace=namespace
1179 )
1180
1181 if vnf_id is None or len(vnf_id) == 0:
1182 vnf_id = ""
1183 else:
1184 # Shorten the vnf_id to its last twelve characters
1185 vnf_id = "vnf-" + vnf_id[-12:]
1186
1187 if vdu_id is None or len(vdu_id) == 0:
1188 vdu_id = ""
1189 else:
1190 # Shorten the vdu_id to its last twelve characters
1191 vdu_id = "-vdu-" + vdu_id[-12:]
1192
1193 if vdu_count is None or len(vdu_count) == 0:
1194 vdu_count = ""
1195 else:
1196 vdu_count = "-cnt-" + vdu_count
1197
1198 # Generate a random suffix with 5 characters (the default size used by K8s)
1199 random_suffix = generate_random_alfanum_string(size=5)
1200
1201 application_name = "app-{}{}{}-{}".format(
1202 vnf_id, vdu_id, vdu_count, random_suffix
1203 )
1204
1205 return N2VCJujuConnector._format_app_name(application_name)
1206
1207 @staticmethod
1208 def _format_model_name(name: str) -> str:
1209 """Format the name of the model.
1210
1211 Model names may only contain lowercase letters, digits and hyphens
1212 """
1213
1214 return name.replace("_", "-").replace(" ", "-").lower()
1215
1216 @staticmethod
1217 def _format_app_name(name: str) -> str:
1218 """Format the name of the application (in order to assure valid application name).
1219
1220 Application names have restrictions (run juju deploy --help):
1221 - contains lowercase letters 'a'-'z'
1222 - contains numbers '0'-'9'
1223 - contains hyphens '-'
1224 - starts with a lowercase letter
1225 - not two or more consecutive hyphens
1226 - after a hyphen, not a group with all numbers
1227 """
1228
1229 def all_numbers(s: str) -> bool:
1230 for c in s:
1231 if not c.isdigit():
1232 return False
1233 return True
1234
1235 new_name = name.replace("_", "-")
1236 new_name = new_name.replace(" ", "-")
1237 new_name = new_name.lower()
1238 while new_name.find("--") >= 0:
1239 new_name = new_name.replace("--", "-")
1240 groups = new_name.split("-")
1241
1242 # find 'all numbers' groups and prefix them with a letter
1243 app_name = ""
1244 for i in range(len(groups)):
1245 group = groups[i]
1246 if all_numbers(group):
1247 group = "z" + group
1248 if i > 0:
1249 app_name += "-"
1250 app_name += group
1251
1252 if app_name[0].isdigit():
1253 app_name = "z" + app_name
1254
1255 return app_name
1256
1257 async def validate_vca(self, vca_id: str):
1258 """
1259 Validate a VCA by connecting/disconnecting to/from it
1260
1261 :param: vca_id: VCA ID
1262 """
1263 vca_connection = await get_connection(self._store, vca_id=vca_id)
1264 libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log, n2vc=self)
1265 controller = await libjuju.get_controller()
1266 await libjuju.disconnect_controller(controller)