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