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