dc4f88309755a51fd75b35a601949c47fb4205f5
[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 db_dict=db_dict,
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>
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 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
1199
1200 return N2VCJujuConnector._format_app_name(application_name)
1201
1202 @staticmethod
1203 def _format_model_name(name: str) -> str:
1204 """Format the name of the model.
1205
1206 Model names may only contain lowercase letters, digits and hyphens
1207 """
1208
1209 return name.replace("_", "-").replace(" ", "-").lower()
1210
1211 @staticmethod
1212 def _format_app_name(name: str) -> str:
1213 """Format the name of the application (in order to assure valid application name).
1214
1215 Application names have restrictions (run juju deploy --help):
1216 - contains lowercase letters 'a'-'z'
1217 - contains numbers '0'-'9'
1218 - contains hyphens '-'
1219 - starts with a lowercase letter
1220 - not two or more consecutive hyphens
1221 - after a hyphen, not a group with all numbers
1222 """
1223
1224 def all_numbers(s: str) -> bool:
1225 for c in s:
1226 if not c.isdigit():
1227 return False
1228 return True
1229
1230 new_name = name.replace("_", "-")
1231 new_name = new_name.replace(" ", "-")
1232 new_name = new_name.lower()
1233 while new_name.find("--") >= 0:
1234 new_name = new_name.replace("--", "-")
1235 groups = new_name.split("-")
1236
1237 # find 'all numbers' groups and prefix them with a letter
1238 app_name = ""
1239 for i in range(len(groups)):
1240 group = groups[i]
1241 if all_numbers(group):
1242 group = "z" + group
1243 if i > 0:
1244 app_name += "-"
1245 app_name += group
1246
1247 if app_name[0].isdigit():
1248 app_name = "z" + app_name
1249
1250 return app_name
1251
1252 async def validate_vca(self, vca_id: str):
1253 """
1254 Validate a VCA by connecting/disconnecting to/from it
1255
1256 :param: vca_id: VCA ID
1257 """
1258 vca_connection = await get_connection(self._store, vca_id=vca_id)
1259 libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log, n2vc=self)
1260 controller = await libjuju.get_controller()
1261 await libjuju.disconnect_controller(controller)