Fix black issues
[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 self.delete_namespace_locks = {}
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 will_not_delete = False
815 if namespace not in self.delete_namespace_locks:
816 self.delete_namespace_locks[namespace] = asyncio.Lock(loop=self.loop)
817 delete_lock = self.delete_namespace_locks[namespace]
818
819 while delete_lock.locked():
820 will_not_delete = True
821 await asyncio.sleep(0.1)
822
823 if will_not_delete:
824 self.log.info("Namespace {} deleted by another worker.".format(namespace))
825 return
826
827 try:
828 async with delete_lock:
829 libjuju = await self._get_libjuju(vca_id)
830
831 # check arguments
832 if namespace is None:
833 raise N2VCBadArgumentsException(
834 message="namespace is mandatory", bad_args=["namespace"]
835 )
836
837 (
838 _nsi_id,
839 ns_id,
840 _vnf_id,
841 _vdu_id,
842 _vdu_count,
843 ) = self._get_namespace_components(namespace=namespace)
844 if ns_id is not None:
845 try:
846 models = await libjuju.list_models(contains=ns_id)
847 for model in models:
848 await libjuju.destroy_model(
849 model_name=model, total_timeout=total_timeout
850 )
851 except Exception as e:
852 raise N2VCException(
853 message="Error deleting namespace {} : {}".format(
854 namespace, e
855 )
856 )
857 else:
858 raise N2VCBadArgumentsException(
859 message="only ns_id is permitted to delete yet",
860 bad_args=["namespace"],
861 )
862 finally:
863 self.delete_namespace_locks.pop(namespace)
864 self.log.info("Namespace {} deleted".format(namespace))
865
866 async def delete_execution_environment(
867 self,
868 ee_id: str,
869 db_dict: dict = None,
870 total_timeout: float = None,
871 scaling_in: bool = False,
872 vca_type: str = None,
873 vca_id: str = None,
874 ):
875 """
876 Delete an execution environment
877 :param str ee_id: id of the execution environment to delete
878 :param dict db_dict: where to write into database when the status changes.
879 It contains a dict with
880 {collection: <str>, filter: {}, path: <str>},
881 e.g. {collection: "nsrs", filter:
882 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
883 :param: total_timeout: Total timeout
884 :param: scaling_in: Boolean to indicate if it is a scaling in operation
885 :param: vca_type: VCA type
886 :param: vca_id: VCA ID
887 """
888 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
889 libjuju = await self._get_libjuju(vca_id)
890
891 # check arguments
892 if ee_id is None:
893 raise N2VCBadArgumentsException(
894 message="ee_id is mandatory", bad_args=["ee_id"]
895 )
896
897 model_name, application_name, machine_id = self._get_ee_id_components(
898 ee_id=ee_id
899 )
900 try:
901 if not scaling_in:
902 # destroy the model
903 await libjuju.destroy_model(
904 model_name=model_name,
905 total_timeout=total_timeout,
906 )
907 elif vca_type == "native_charm" and scaling_in:
908 # destroy the unit in the application
909 await libjuju.destroy_unit(
910 application_name=application_name,
911 model_name=model_name,
912 machine_id=machine_id,
913 total_timeout=total_timeout,
914 )
915 else:
916 # destroy the application
917 await libjuju.destroy_application(
918 model_name=model_name,
919 application_name=application_name,
920 total_timeout=total_timeout,
921 )
922 except Exception as e:
923 raise N2VCException(
924 message=(
925 "Error deleting execution environment {} (application {}) : {}"
926 ).format(ee_id, application_name, e)
927 )
928
929 self.log.info("Execution environment {} deleted".format(ee_id))
930
931 async def exec_primitive(
932 self,
933 ee_id: str,
934 primitive_name: str,
935 params_dict: dict,
936 db_dict: dict = None,
937 progress_timeout: float = None,
938 total_timeout: float = None,
939 vca_id: str = None,
940 vca_type: str = None,
941 ) -> str:
942 """
943 Execute a primitive in the execution environment
944
945 :param: ee_id: the one returned by create_execution_environment or
946 register_execution_environment
947 :param: primitive_name: must be one defined in the software. There is one
948 called 'config', where, for the proxy case, the 'credentials' of VM are
949 provided
950 :param: params_dict: parameters of the action
951 :param: db_dict: where to write into database when the status changes.
952 It contains a dict with
953 {collection: <str>, filter: {}, path: <str>},
954 e.g. {collection: "nsrs", filter:
955 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
956 :param: progress_timeout: Progress timeout
957 :param: total_timeout: Total timeout
958 :param: vca_id: VCA ID
959 :param: vca_type: VCA type
960 :returns str: primitive result, if ok. It raises exceptions in case of fail
961 """
962
963 self.log.info(
964 "Executing primitive: {} on ee: {}, params: {}".format(
965 primitive_name, ee_id, params_dict
966 )
967 )
968 libjuju = await self._get_libjuju(vca_id)
969
970 # check arguments
971 if ee_id is None or len(ee_id) == 0:
972 raise N2VCBadArgumentsException(
973 message="ee_id is mandatory", bad_args=["ee_id"]
974 )
975 if primitive_name is None or len(primitive_name) == 0:
976 raise N2VCBadArgumentsException(
977 message="action_name is mandatory", bad_args=["action_name"]
978 )
979 if params_dict is None:
980 params_dict = dict()
981
982 try:
983 (
984 model_name,
985 application_name,
986 machine_id,
987 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
988 # To run action on the leader unit in libjuju.execute_action function,
989 # machine_id must be set to None if vca_type is not native_charm
990 if vca_type != "native_charm":
991 machine_id = None
992 except Exception:
993 raise N2VCBadArgumentsException(
994 message="ee_id={} is not a valid execution environment id".format(
995 ee_id
996 ),
997 bad_args=["ee_id"],
998 )
999
1000 if primitive_name == "config":
1001 # Special case: config primitive
1002 try:
1003 await libjuju.configure_application(
1004 model_name=model_name,
1005 application_name=application_name,
1006 config=params_dict,
1007 )
1008 actions = await libjuju.get_actions(
1009 application_name=application_name,
1010 model_name=model_name,
1011 )
1012 self.log.debug(
1013 "Application {} has these actions: {}".format(
1014 application_name, actions
1015 )
1016 )
1017 if "verify-ssh-credentials" in actions:
1018 # execute verify-credentials
1019 num_retries = 20
1020 retry_timeout = 15.0
1021 for _ in range(num_retries):
1022 try:
1023 self.log.debug("Executing action verify-ssh-credentials...")
1024 output, ok = await libjuju.execute_action(
1025 model_name=model_name,
1026 application_name=application_name,
1027 action_name="verify-ssh-credentials",
1028 db_dict=db_dict,
1029 progress_timeout=progress_timeout,
1030 total_timeout=total_timeout,
1031 )
1032
1033 if ok == "failed":
1034 self.log.debug(
1035 "Error executing verify-ssh-credentials: {}. Retrying..."
1036 )
1037 await asyncio.sleep(retry_timeout)
1038
1039 continue
1040 self.log.debug("Result: {}, output: {}".format(ok, output))
1041 break
1042 except asyncio.CancelledError:
1043 raise
1044 else:
1045 self.log.error(
1046 "Error executing verify-ssh-credentials after {} retries. ".format(
1047 num_retries
1048 )
1049 )
1050 else:
1051 msg = "Action verify-ssh-credentials does not exist in application {}".format(
1052 application_name
1053 )
1054 self.log.debug(msg=msg)
1055 except Exception as e:
1056 self.log.error("Error configuring juju application: {}".format(e))
1057 raise N2VCExecutionException(
1058 message="Error configuring application into ee={} : {}".format(
1059 ee_id, e
1060 ),
1061 primitive_name=primitive_name,
1062 )
1063 return "CONFIG OK"
1064 else:
1065 try:
1066 output, status = await libjuju.execute_action(
1067 model_name=model_name,
1068 application_name=application_name,
1069 action_name=primitive_name,
1070 db_dict=db_dict,
1071 machine_id=machine_id,
1072 progress_timeout=progress_timeout,
1073 total_timeout=total_timeout,
1074 **params_dict
1075 )
1076 if status == "completed":
1077 return output
1078 else:
1079 raise Exception("status is not completed: {}".format(status))
1080 except Exception as e:
1081 self.log.error(
1082 "Error executing primitive {}: {}".format(primitive_name, e)
1083 )
1084 raise N2VCExecutionException(
1085 message="Error executing primitive {} into ee={} : {}".format(
1086 primitive_name, ee_id, e
1087 ),
1088 primitive_name=primitive_name,
1089 )
1090
1091 async def disconnect(self, vca_id: str = None):
1092 """
1093 Disconnect from VCA
1094
1095 :param: vca_id: VCA ID
1096 """
1097 self.log.info("closing juju N2VC...")
1098 libjuju = await self._get_libjuju(vca_id)
1099 try:
1100 await libjuju.disconnect()
1101 except Exception as e:
1102 raise N2VCConnectionException(
1103 message="Error disconnecting controller: {}".format(e),
1104 url=libjuju.vca_connection.data.endpoints,
1105 )
1106
1107 """
1108 ####################################################################################
1109 ################################### P R I V A T E ##################################
1110 ####################################################################################
1111 """
1112
1113 async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
1114 """
1115 Get libjuju object
1116
1117 :param: vca_id: VCA ID
1118 If None, get a libjuju object with a Connection to the default VCA
1119 Else, geta libjuju object with a Connection to the specified VCA
1120 """
1121 if not vca_id:
1122 while self.loading_libjuju.locked():
1123 await asyncio.sleep(0.1)
1124 if not self.libjuju:
1125 async with self.loading_libjuju:
1126 vca_connection = await get_connection(self._store)
1127 self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
1128 return self.libjuju
1129 else:
1130 vca_connection = await get_connection(self._store, vca_id)
1131 return Libjuju(
1132 vca_connection,
1133 loop=self.loop,
1134 log=self.log,
1135 n2vc=self,
1136 )
1137
1138 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
1139 # write ee_id to database: _admin.deployed.VCA.x
1140 try:
1141 the_table = db_dict["collection"]
1142 the_filter = db_dict["filter"]
1143 the_path = db_dict["path"]
1144 if not the_path[-1] == ".":
1145 the_path = the_path + "."
1146 update_dict = {the_path + "ee_id": ee_id}
1147 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1148 self.db.set_one(
1149 table=the_table,
1150 q_filter=the_filter,
1151 update_dict=update_dict,
1152 fail_on_empty=True,
1153 )
1154 except asyncio.CancelledError:
1155 raise
1156 except Exception as e:
1157 self.log.error("Error writing ee_id to database: {}".format(e))
1158
1159 @staticmethod
1160 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
1161 """
1162 Build an execution environment id form model, application and machine
1163 :param model_name:
1164 :param application_name:
1165 :param machine_id:
1166 :return:
1167 """
1168 # id for the execution environment
1169 return "{}.{}.{}".format(model_name, application_name, machine_id)
1170
1171 @staticmethod
1172 def _get_ee_id_components(ee_id: str) -> (str, str, str):
1173 """
1174 Get model, application and machine components from an execution environment id
1175 :param ee_id:
1176 :return: model_name, application_name, machine_id
1177 """
1178
1179 if ee_id is None:
1180 return None, None, None
1181
1182 # split components of id
1183 parts = ee_id.split(".")
1184 model_name = parts[0]
1185 application_name = parts[1]
1186 machine_id = parts[2]
1187 return model_name, application_name, machine_id
1188
1189 def _get_application_name(self, namespace: str) -> str:
1190 """
1191 Build application name from namespace
1192 :param namespace:
1193 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
1194 """
1195
1196 # TODO: Enforce the Juju 50-character application limit
1197
1198 # split namespace components
1199 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1200 namespace=namespace
1201 )
1202
1203 if vnf_id is None or len(vnf_id) == 0:
1204 vnf_id = ""
1205 else:
1206 # Shorten the vnf_id to its last twelve characters
1207 vnf_id = "vnf-" + vnf_id[-12:]
1208
1209 if vdu_id is None or len(vdu_id) == 0:
1210 vdu_id = ""
1211 else:
1212 # Shorten the vdu_id to its last twelve characters
1213 vdu_id = "-vdu-" + vdu_id[-12:]
1214
1215 if vdu_count is None or len(vdu_count) == 0:
1216 vdu_count = ""
1217 else:
1218 vdu_count = "-cnt-" + vdu_count
1219
1220 # Generate a random suffix with 5 characters (the default size used by K8s)
1221 random_suffix = generate_random_alfanum_string(size=5)
1222
1223 application_name = "app-{}{}{}-{}".format(
1224 vnf_id, vdu_id, vdu_count, random_suffix
1225 )
1226
1227 return N2VCJujuConnector._format_app_name(application_name)
1228
1229 @staticmethod
1230 def _format_model_name(name: str) -> str:
1231 """Format the name of the model.
1232
1233 Model names may only contain lowercase letters, digits and hyphens
1234 """
1235
1236 return name.replace("_", "-").replace(" ", "-").lower()
1237
1238 @staticmethod
1239 def _format_app_name(name: str) -> str:
1240 """Format the name of the application (in order to assure valid application name).
1241
1242 Application names have restrictions (run juju deploy --help):
1243 - contains lowercase letters 'a'-'z'
1244 - contains numbers '0'-'9'
1245 - contains hyphens '-'
1246 - starts with a lowercase letter
1247 - not two or more consecutive hyphens
1248 - after a hyphen, not a group with all numbers
1249 """
1250
1251 def all_numbers(s: str) -> bool:
1252 for c in s:
1253 if not c.isdigit():
1254 return False
1255 return True
1256
1257 new_name = name.replace("_", "-")
1258 new_name = new_name.replace(" ", "-")
1259 new_name = new_name.lower()
1260 while new_name.find("--") >= 0:
1261 new_name = new_name.replace("--", "-")
1262 groups = new_name.split("-")
1263
1264 # find 'all numbers' groups and prefix them with a letter
1265 app_name = ""
1266 for i in range(len(groups)):
1267 group = groups[i]
1268 if all_numbers(group):
1269 group = "z" + group
1270 if i > 0:
1271 app_name += "-"
1272 app_name += group
1273
1274 if app_name[0].isdigit():
1275 app_name = "z" + app_name
1276
1277 return app_name
1278
1279 async def validate_vca(self, vca_id: str):
1280 """
1281 Validate a VCA by connecting/disconnecting to/from it
1282
1283 :param: vca_id: VCA ID
1284 """
1285 vca_connection = await get_connection(self._store, vca_id=vca_id)
1286 libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log, n2vc=self)
1287 controller = await libjuju.get_controller()
1288 await libjuju.disconnect_controller(controller)