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