Add deleting lock to K8sJujuConnector
[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 raise N2VCException(
833 message="Error deleting namespace {} : {}".format(
834 namespace, e
835 )
836 )
837 else:
838 raise N2VCBadArgumentsException(
839 message="only ns_id is permitted to delete yet",
840 bad_args=["namespace"],
841 )
842 finally:
843 self.delete_namespace_locks.pop(namespace)
844 self.log.info("Namespace {} deleted".format(namespace))
845
846 async def delete_execution_environment(
847 self,
848 ee_id: str,
849 db_dict: dict = None,
850 total_timeout: float = None,
851 scaling_in: bool = False,
852 vca_type: str = None,
853 vca_id: str = None,
854 ):
855 """
856 Delete an execution environment
857 :param str ee_id: id of the execution environment to delete
858 :param dict db_dict: where to write into database when the status changes.
859 It contains a dict with
860 {collection: <str>, filter: {}, path: <str>},
861 e.g. {collection: "nsrs", filter:
862 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
863 :param: total_timeout: Total timeout
864 :param: scaling_in: Boolean to indicate if it is a scaling in operation
865 :param: vca_type: VCA type
866 :param: vca_id: VCA ID
867 """
868 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
869 libjuju = await self._get_libjuju(vca_id)
870
871 # check arguments
872 if ee_id is None:
873 raise N2VCBadArgumentsException(
874 message="ee_id is mandatory", bad_args=["ee_id"]
875 )
876
877 model_name, application_name, machine_id = self._get_ee_id_components(
878 ee_id=ee_id
879 )
880 try:
881 if not scaling_in:
882 # destroy the model
883 await libjuju.destroy_model(
884 model_name=model_name,
885 total_timeout=total_timeout,
886 )
887 elif vca_type == "native_charm" and scaling_in:
888 # destroy the unit in the application
889 await libjuju.destroy_unit(
890 application_name=application_name,
891 model_name=model_name,
892 machine_id=machine_id,
893 total_timeout=total_timeout,
894 )
895 else:
896 # destroy the application
897 await libjuju.destroy_application(
898 model_name=model_name,
899 application_name=application_name,
900 total_timeout=total_timeout,
901 )
902 except Exception as e:
903 raise N2VCException(
904 message=(
905 "Error deleting execution environment {} (application {}) : {}"
906 ).format(ee_id, application_name, e)
907 )
908
909 self.log.info("Execution environment {} deleted".format(ee_id))
910
911 async def exec_primitive(
912 self,
913 ee_id: str,
914 primitive_name: str,
915 params_dict: dict,
916 db_dict: dict = None,
917 progress_timeout: float = None,
918 total_timeout: float = None,
919 vca_id: str = None,
920 vca_type: str = None,
921 ) -> str:
922 """
923 Execute a primitive in the execution environment
924
925 :param: ee_id: the one returned by create_execution_environment or
926 register_execution_environment
927 :param: primitive_name: must be one defined in the software. There is one
928 called 'config', where, for the proxy case, the 'credentials' of VM are
929 provided
930 :param: params_dict: parameters of the action
931 :param: db_dict: where to write into database when the status changes.
932 It contains a dict with
933 {collection: <str>, filter: {}, path: <str>},
934 e.g. {collection: "nsrs", filter:
935 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
936 :param: progress_timeout: Progress timeout
937 :param: total_timeout: Total timeout
938 :param: vca_id: VCA ID
939 :param: vca_type: VCA type
940 :returns str: primitive result, if ok. It raises exceptions in case of fail
941 """
942
943 self.log.info(
944 "Executing primitive: {} on ee: {}, params: {}".format(
945 primitive_name, ee_id, params_dict
946 )
947 )
948 libjuju = await self._get_libjuju(vca_id)
949
950 # check arguments
951 if ee_id is None or len(ee_id) == 0:
952 raise N2VCBadArgumentsException(
953 message="ee_id is mandatory", bad_args=["ee_id"]
954 )
955 if primitive_name is None or len(primitive_name) == 0:
956 raise N2VCBadArgumentsException(
957 message="action_name is mandatory", bad_args=["action_name"]
958 )
959 if params_dict is None:
960 params_dict = dict()
961
962 try:
963 (
964 model_name,
965 application_name,
966 machine_id,
967 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
968 # To run action on the leader unit in libjuju.execute_action function,
969 # machine_id must be set to None if vca_type is not native_charm
970 if vca_type != "native_charm":
971 machine_id = None
972 except Exception:
973 raise N2VCBadArgumentsException(
974 message="ee_id={} is not a valid execution environment id".format(
975 ee_id
976 ),
977 bad_args=["ee_id"],
978 )
979
980 if primitive_name == "config":
981 # Special case: config primitive
982 try:
983 await libjuju.configure_application(
984 model_name=model_name,
985 application_name=application_name,
986 config=params_dict,
987 )
988 actions = await libjuju.get_actions(
989 application_name=application_name,
990 model_name=model_name,
991 )
992 self.log.debug(
993 "Application {} has these actions: {}".format(
994 application_name, actions
995 )
996 )
997 if "verify-ssh-credentials" in actions:
998 # execute verify-credentials
999 num_retries = 20
1000 retry_timeout = 15.0
1001 for _ in range(num_retries):
1002 try:
1003 self.log.debug("Executing action verify-ssh-credentials...")
1004 output, ok = await libjuju.execute_action(
1005 model_name=model_name,
1006 application_name=application_name,
1007 action_name="verify-ssh-credentials",
1008 db_dict=db_dict,
1009 progress_timeout=progress_timeout,
1010 total_timeout=total_timeout,
1011 )
1012
1013 if ok == "failed":
1014 self.log.debug(
1015 "Error executing verify-ssh-credentials: {}. Retrying..."
1016 )
1017 await asyncio.sleep(retry_timeout)
1018
1019 continue
1020 self.log.debug("Result: {}, output: {}".format(ok, output))
1021 break
1022 except asyncio.CancelledError:
1023 raise
1024 else:
1025 self.log.error(
1026 "Error executing verify-ssh-credentials after {} retries. ".format(
1027 num_retries
1028 )
1029 )
1030 else:
1031 msg = "Action verify-ssh-credentials does not exist in application {}".format(
1032 application_name
1033 )
1034 self.log.debug(msg=msg)
1035 except Exception as e:
1036 self.log.error("Error configuring juju application: {}".format(e))
1037 raise N2VCExecutionException(
1038 message="Error configuring application into ee={} : {}".format(
1039 ee_id, e
1040 ),
1041 primitive_name=primitive_name,
1042 )
1043 return "CONFIG OK"
1044 else:
1045 try:
1046 output, status = await libjuju.execute_action(
1047 model_name=model_name,
1048 application_name=application_name,
1049 action_name=primitive_name,
1050 db_dict=db_dict,
1051 machine_id=machine_id,
1052 progress_timeout=progress_timeout,
1053 total_timeout=total_timeout,
1054 **params_dict,
1055 )
1056 if status == "completed":
1057 return output
1058 else:
1059 raise Exception("status is not completed: {}".format(status))
1060 except Exception as e:
1061 self.log.error(
1062 "Error executing primitive {}: {}".format(primitive_name, e)
1063 )
1064 raise N2VCExecutionException(
1065 message="Error executing primitive {} into ee={} : {}".format(
1066 primitive_name, ee_id, e
1067 ),
1068 primitive_name=primitive_name,
1069 )
1070
1071 async def disconnect(self, vca_id: str = None):
1072 """
1073 Disconnect from VCA
1074
1075 :param: vca_id: VCA ID
1076 """
1077 self.log.info("closing juju N2VC...")
1078 libjuju = await self._get_libjuju(vca_id)
1079 try:
1080 await libjuju.disconnect()
1081 except Exception as e:
1082 raise N2VCConnectionException(
1083 message="Error disconnecting controller: {}".format(e),
1084 url=libjuju.vca_connection.data.endpoints,
1085 )
1086
1087 """
1088 ####################################################################################
1089 ################################### P R I V A T E ##################################
1090 ####################################################################################
1091 """
1092
1093 async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
1094 """
1095 Get libjuju object
1096
1097 :param: vca_id: VCA ID
1098 If None, get a libjuju object with a Connection to the default VCA
1099 Else, geta libjuju object with a Connection to the specified VCA
1100 """
1101 if not vca_id:
1102 while self.loading_libjuju.locked():
1103 await asyncio.sleep(0.1)
1104 if not self.libjuju:
1105 async with self.loading_libjuju:
1106 vca_connection = await get_connection(self._store)
1107 self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
1108 return self.libjuju
1109 else:
1110 vca_connection = await get_connection(self._store, vca_id)
1111 return Libjuju(
1112 vca_connection,
1113 loop=self.loop,
1114 log=self.log,
1115 n2vc=self,
1116 )
1117
1118 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
1119
1120 # write ee_id to database: _admin.deployed.VCA.x
1121 try:
1122 the_table = db_dict["collection"]
1123 the_filter = db_dict["filter"]
1124 the_path = db_dict["path"]
1125 if not the_path[-1] == ".":
1126 the_path = the_path + "."
1127 update_dict = {the_path + "ee_id": ee_id}
1128 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
1129 self.db.set_one(
1130 table=the_table,
1131 q_filter=the_filter,
1132 update_dict=update_dict,
1133 fail_on_empty=True,
1134 )
1135 except asyncio.CancelledError:
1136 raise
1137 except Exception as e:
1138 self.log.error("Error writing ee_id to database: {}".format(e))
1139
1140 @staticmethod
1141 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
1142 """
1143 Build an execution environment id form model, application and machine
1144 :param model_name:
1145 :param application_name:
1146 :param machine_id:
1147 :return:
1148 """
1149 # id for the execution environment
1150 return "{}.{}.{}".format(model_name, application_name, machine_id)
1151
1152 @staticmethod
1153 def _get_ee_id_components(ee_id: str) -> (str, str, str):
1154 """
1155 Get model, application and machine components from an execution environment id
1156 :param ee_id:
1157 :return: model_name, application_name, machine_id
1158 """
1159
1160 return get_ee_id_components(ee_id)
1161
1162 def _get_application_name(self, namespace: str) -> str:
1163 """
1164 Build application name from namespace
1165 :param namespace:
1166 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
1167 """
1168
1169 # TODO: Enforce the Juju 50-character application limit
1170
1171 # split namespace components
1172 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1173 namespace=namespace
1174 )
1175
1176 if vnf_id is None or len(vnf_id) == 0:
1177 vnf_id = ""
1178 else:
1179 # Shorten the vnf_id to its last twelve characters
1180 vnf_id = "vnf-" + vnf_id[-12:]
1181
1182 if vdu_id is None or len(vdu_id) == 0:
1183 vdu_id = ""
1184 else:
1185 # Shorten the vdu_id to its last twelve characters
1186 vdu_id = "-vdu-" + vdu_id[-12:]
1187
1188 if vdu_count is None or len(vdu_count) == 0:
1189 vdu_count = ""
1190 else:
1191 vdu_count = "-cnt-" + vdu_count
1192
1193 # Generate a random suffix with 5 characters (the default size used by K8s)
1194 random_suffix = generate_random_alfanum_string(size=5)
1195
1196 application_name = "app-{}{}{}-{}".format(
1197 vnf_id, vdu_id, vdu_count, random_suffix
1198 )
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)