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