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