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