2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
31 from juju
.action
import Action
32 from juju
.application
import Application
33 from juju
.client
import client
34 from juju
.controller
import Controller
35 from juju
.errors
import JujuAPIError
36 from juju
.machine
import Machine
37 from juju
.model
import Model
38 from n2vc
.exceptions
import (
39 N2VCBadArgumentsException
,
41 N2VCConnectionException
,
42 N2VCExecutionException
,
43 N2VCInvalidCertificate
,
46 JujuK8sProxycharmNotSupported
,
48 from n2vc
.juju_observer
import JujuModelObserver
49 from n2vc
.n2vc_conn
import N2VCConnector
50 from n2vc
.n2vc_conn
import obj_to_dict
, obj_to_yaml
51 from n2vc
.provisioner
import AsyncSSHProvisioner
52 from n2vc
.libjuju
import Libjuju
55 class N2VCJujuConnector(N2VCConnector
):
58 ####################################################################################
59 ################################### P U B L I C ####################################
60 ####################################################################################
63 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
71 url
: str = "127.0.0.1:17070",
72 username
: str = "admin",
73 vca_config
: dict = None,
76 """Initialize juju N2VC connector
79 # parent class constructor
80 N2VCConnector
.__init
__(
88 vca_config
=vca_config
,
89 on_update_db
=on_update_db
,
92 # silence websocket traffic log
93 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
94 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
95 logging
.getLogger("model").setLevel(logging
.WARN
)
97 self
.log
.info("Initializing N2VC juju connector...")
100 ##############################################################
102 ##############################################################
107 raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
108 url_parts
= url
.split(":")
109 if len(url_parts
) != 2:
110 raise N2VCBadArgumentsException(
111 "Argument url: bad format (localhost:port) -> {}".format(url
), ["url"]
113 self
.hostname
= url_parts
[0]
115 self
.port
= int(url_parts
[1])
117 raise N2VCBadArgumentsException(
118 "url port must be a number -> {}".format(url
), ["url"]
123 raise N2VCBadArgumentsException(
124 "Argument username is mandatory", ["username"]
128 if vca_config
is None:
129 raise N2VCBadArgumentsException(
130 "Argument vca_config is mandatory", ["vca_config"]
133 if "secret" in vca_config
:
134 self
.secret
= vca_config
["secret"]
136 raise N2VCBadArgumentsException(
137 "Argument vca_config.secret is mandatory", ["vca_config.secret"]
140 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
141 # if exists, it will be written in lcm container: _create_juju_public_key()
142 if "public_key" in vca_config
:
143 self
.public_key
= vca_config
["public_key"]
145 self
.public_key
= None
147 # TODO: Verify ca_cert is valid before using. VCA will crash
148 # if the ca_cert isn't formatted correctly.
149 def base64_to_cacert(b64string
):
150 """Convert the base64-encoded string containing the VCA CACERT.
156 cacert
= base64
.b64decode(b64string
).decode("utf-8")
158 cacert
= re
.sub(r
"\\n", r
"\n", cacert
,)
159 except binascii
.Error
as e
:
160 self
.log
.debug("Caught binascii.Error: {}".format(e
))
161 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
165 self
.ca_cert
= vca_config
.get("ca_cert")
167 self
.ca_cert
= base64_to_cacert(vca_config
["ca_cert"])
169 if "api_proxy" in vca_config
and vca_config
["api_proxy"] != "":
170 self
.api_proxy
= vca_config
["api_proxy"]
172 "api_proxy for native charms configured: {}".format(self
.api_proxy
)
176 "api_proxy is not configured"
178 self
.api_proxy
= None
180 if "enable_os_upgrade" in vca_config
:
181 self
.enable_os_upgrade
= vca_config
["enable_os_upgrade"]
183 self
.enable_os_upgrade
= True
185 if "apt_mirror" in vca_config
:
186 self
.apt_mirror
= vca_config
["apt_mirror"]
188 self
.apt_mirror
= None
190 self
.cloud
= vca_config
.get('cloud')
191 self
.k8s_cloud
= None
192 if "k8s_cloud" in vca_config
:
193 self
.k8s_cloud
= vca_config
.get("k8s_cloud")
194 self
.log
.debug('Arguments have been checked')
197 self
.controller
= None # it will be filled when connect to juju
198 self
.juju_models
= {} # model objects for every model_name
199 self
.juju_observers
= {} # model observers for every model_name
201 False # while connecting to juju (to avoid duplicate connections)
203 self
._authenticated
= (
204 False # it will be True when juju connection be stablished
206 self
._creating
_model
= False # True during model creation
207 self
.libjuju
= Libjuju(
209 api_proxy
=self
.api_proxy
,
210 enable_os_upgrade
=self
.enable_os_upgrade
,
211 apt_mirror
=self
.apt_mirror
,
212 username
=self
.username
,
213 password
=self
.secret
,
221 # create juju pub key file in lcm container at
222 # ./local/share/juju/ssh/juju_id_rsa.pub
223 self
._create
_juju
_public
_key
()
225 self
.log
.info("N2VC juju connector initialized")
227 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
229 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
231 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
234 # model name is ns_id
236 if model_name
is None:
237 msg
= "Namespace {} not valid".format(namespace
)
239 raise N2VCBadArgumentsException(msg
, ["namespace"])
242 models
= await self
.libjuju
.list_models(contains
=ns_id
)
245 status
[m
] = await self
.libjuju
.get_model_status(m
)
247 return obj_to_yaml(status
)
249 return obj_to_dict(status
)
251 async def update_vca_status(self
, vcastatus
: dict):
253 Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
254 :param vcastatus: dict containing vcaStatus
258 for model_name
in vcastatus
:
259 # Adding executed actions
260 vcastatus
[model_name
]["executedActions"] = \
261 await self
.libjuju
.get_executed_actions(model_name
)
262 for application
in vcastatus
[model_name
]["applications"]:
263 # Adding application actions
264 vcastatus
[model_name
]["applications"][application
]["actions"] = \
265 await self
.libjuju
.get_actions(application
, model_name
)
266 # Adding application configs
267 vcastatus
[model_name
]["applications"][application
]["configs"] = \
268 await self
.libjuju
.get_application_configs(model_name
, application
)
269 except Exception as e
:
270 self
.log
.debug("Error in updating vca status: {}".format(str(e
)))
272 async def create_execution_environment(
276 reuse_ee_id
: str = None,
277 progress_timeout
: float = None,
278 total_timeout
: float = None,
282 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
283 namespace
, reuse_ee_id
289 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
299 ) = self
._get
_namespace
_components
(namespace
=namespace
)
300 # model name is ns_id
303 application_name
= self
._get
_application
_name
(namespace
=namespace
)
306 "model name: {}, application name: {}, machine_id: {}".format(
307 model_name
, application_name
, machine_id
311 # create or reuse a new juju machine
313 if not await self
.libjuju
.model_exists(model_name
):
314 await self
.libjuju
.add_model(model_name
, cloud_name
=self
.cloud
)
315 machine
, new
= await self
.libjuju
.create_machine(
316 model_name
=model_name
,
317 machine_id
=machine_id
,
319 progress_timeout
=progress_timeout
,
320 total_timeout
=total_timeout
,
322 # id for the execution environment
323 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
324 model_name
=model_name
,
325 application_name
=application_name
,
326 machine_id
=str(machine
.entity_id
),
328 self
.log
.debug("ee_id: {}".format(ee_id
))
331 # write ee_id in database
332 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
334 except Exception as e
:
335 message
= "Error creating machine on juju: {}".format(e
)
336 self
.log
.error(message
)
337 raise N2VCException(message
=message
)
339 # new machine credentials
341 "hostname": machine
.dns_name
,
345 "Execution environment created. ee_id: {}, credentials: {}".format(
350 return ee_id
, credentials
352 async def register_execution_environment(
357 progress_timeout
: float = None,
358 total_timeout
: float = None,
362 "Registering execution environment. namespace={}, credentials={}".format(
363 namespace
, credentials
367 if credentials
is None:
368 raise N2VCBadArgumentsException(
369 message
="credentials are mandatory", bad_args
=["credentials"]
371 if credentials
.get("hostname"):
372 hostname
= credentials
["hostname"]
374 raise N2VCBadArgumentsException(
375 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
377 if credentials
.get("username"):
378 username
= credentials
["username"]
380 raise N2VCBadArgumentsException(
381 message
="username is mandatory", bad_args
=["credentials.username"]
383 if "private_key_path" in credentials
:
384 private_key_path
= credentials
["private_key_path"]
386 # if not passed as argument, use generated private key path
387 private_key_path
= self
.private_key_path
389 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
396 application_name
= self
._get
_application
_name
(namespace
=namespace
)
398 # register machine on juju
400 if not await self
.libjuju
.model_exists(model_name
):
401 await self
.libjuju
.add_model(model_name
, cloud_name
=self
.cloud
)
402 machine_id
= await self
.libjuju
.provision_machine(
403 model_name
=model_name
,
406 private_key_path
=private_key_path
,
408 progress_timeout
=progress_timeout
,
409 total_timeout
=total_timeout
,
411 except Exception as e
:
412 self
.log
.error("Error registering machine: {}".format(e
))
414 message
="Error registering machine on juju: {}".format(e
)
417 self
.log
.info("Machine registered: {}".format(machine_id
))
419 # id for the execution environment
420 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
421 model_name
=model_name
,
422 application_name
=application_name
,
423 machine_id
=str(machine_id
),
426 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
430 async def install_configuration_sw(
435 progress_timeout
: float = None,
436 total_timeout
: float = None,
443 "Installing configuration sw on ee_id: {}, "
444 "artifact path: {}, db_dict: {}"
445 ).format(ee_id
, artifact_path
, db_dict
)
449 if ee_id
is None or len(ee_id
) == 0:
450 raise N2VCBadArgumentsException(
451 message
="ee_id is mandatory", bad_args
=["ee_id"]
453 if artifact_path
is None or len(artifact_path
) == 0:
454 raise N2VCBadArgumentsException(
455 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
458 raise N2VCBadArgumentsException(
459 message
="db_dict is mandatory", bad_args
=["db_dict"]
467 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
469 "model: {}, application: {}, machine: {}".format(
470 model_name
, application_name
, machine_id
474 raise N2VCBadArgumentsException(
475 message
="ee_id={} is not a valid execution environment id".format(
481 # remove // in charm path
482 while artifact_path
.find("//") >= 0:
483 artifact_path
= artifact_path
.replace("//", "/")
486 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
487 msg
= "artifact path does not exist: {}".format(artifact_path
)
488 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
490 if artifact_path
.startswith("/"):
491 full_path
= self
.fs
.path
+ artifact_path
493 full_path
= self
.fs
.path
+ "/" + artifact_path
496 await self
.libjuju
.deploy_charm(
497 model_name
=model_name
,
498 application_name
=application_name
,
500 machine_id
=machine_id
,
502 progress_timeout
=progress_timeout
,
503 total_timeout
=total_timeout
,
507 except Exception as e
:
509 message
="Error desploying charm into ee={} : {}".format(ee_id
, e
)
512 self
.log
.info("Configuration sw installed")
514 async def install_k8s_proxy_charm(
520 progress_timeout
: float = None,
521 total_timeout
: float = None,
525 Install a k8s proxy charm
527 :param charm_name: Name of the charm being deployed
528 :param namespace: collection of all the uuids related to the charm.
529 :param str artifact_path: where to locate the artifacts (parent folder) using
531 the final artifact path will be a combination of this artifact_path and
532 additional string from the config_dict (e.g. charm name)
533 :param dict db_dict: where to write into database when the status changes.
534 It contains a dict with
535 {collection: <str>, filter: {}, path: <str>},
536 e.g. {collection: "nsrs", filter:
537 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
538 :param float progress_timeout:
539 :param float total_timeout:
540 :param config: Dictionary with additional configuration
542 :returns ee_id: execution environment id.
544 self
.log
.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
545 .format(charm_name
, artifact_path
, db_dict
))
547 if not self
.k8s_cloud
:
548 raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
550 if artifact_path
is None or len(artifact_path
) == 0:
551 raise N2VCBadArgumentsException(
552 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
555 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
557 # remove // in charm path
558 while artifact_path
.find('//') >= 0:
559 artifact_path
= artifact_path
.replace('//', '/')
562 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
563 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
564 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
566 if artifact_path
.startswith('/'):
567 full_path
= self
.fs
.path
+ artifact_path
569 full_path
= self
.fs
.path
+ '/' + artifact_path
571 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
572 model_name
= '{}-k8s'.format(ns_id
)
574 await self
.libjuju
.add_model(model_name
, self
.k8s_cloud
)
575 application_name
= self
._get
_application
_name
(namespace
)
578 await self
.libjuju
.deploy_charm(
579 model_name
=model_name
,
580 application_name
=application_name
,
584 progress_timeout
=progress_timeout
,
585 total_timeout
=total_timeout
,
588 except Exception as e
:
589 raise N2VCException(message
='Error deploying charm: {}'.format(e
))
591 self
.log
.info('K8s proxy charm installed')
592 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
593 model_name
=model_name
,
594 application_name
=application_name
,
598 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
602 async def get_ee_ssh_public__key(
606 progress_timeout
: float = None,
607 total_timeout
: float = None,
612 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
613 ).format(ee_id
, db_dict
)
617 if ee_id
is None or len(ee_id
) == 0:
618 raise N2VCBadArgumentsException(
619 message
="ee_id is mandatory", bad_args
=["ee_id"]
622 raise N2VCBadArgumentsException(
623 message
="db_dict is mandatory", bad_args
=["db_dict"]
631 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
633 "model: {}, application: {}, machine: {}".format(
634 model_name
, application_name
, machine_id
638 raise N2VCBadArgumentsException(
639 message
="ee_id={} is not a valid execution environment id".format(
645 # try to execute ssh layer primitives (if exist):
651 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
653 # execute action: generate-ssh-key
655 output
, _status
= await self
.libjuju
.execute_action(
656 model_name
=model_name
,
657 application_name
=application_name
,
658 action_name
="generate-ssh-key",
660 progress_timeout
=progress_timeout
,
661 total_timeout
=total_timeout
,
663 except Exception as e
:
665 "Skipping exception while executing action generate-ssh-key: {}".format(
670 # execute action: get-ssh-public-key
672 output
, _status
= await self
.libjuju
.execute_action(
673 model_name
=model_name
,
674 application_name
=application_name
,
675 action_name
="get-ssh-public-key",
677 progress_timeout
=progress_timeout
,
678 total_timeout
=total_timeout
,
680 except Exception as e
:
681 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
683 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
685 # return public key if exists
686 return output
["pubkey"] if "pubkey" in output
else output
688 async def add_relation(
689 self
, ee_id_1
: str, ee_id_2
: str, endpoint_1
: str, endpoint_2
: str
693 "adding new relation between {} and {}, endpoints: {}, {}".format(
694 ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
700 message
= "EE 1 is mandatory"
701 self
.log
.error(message
)
702 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_1"])
704 message
= "EE 2 is mandatory"
705 self
.log
.error(message
)
706 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_2"])
708 message
= "endpoint 1 is mandatory"
709 self
.log
.error(message
)
710 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_1"])
712 message
= "endpoint 2 is mandatory"
713 self
.log
.error(message
)
714 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_2"])
716 # get the model, the applications and the machines from the ee_id's
717 model_1
, app_1
, _machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
718 model_2
, app_2
, _machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
720 # model must be the same
721 if model_1
!= model_2
:
722 message
= "EE models are not the same: {} vs {}".format(ee_id_1
, ee_id_2
)
723 self
.log
.error(message
)
724 raise N2VCBadArgumentsException(
725 message
=message
, bad_args
=["ee_id_1", "ee_id_2"]
728 # add juju relations between two applications
730 await self
.libjuju
.add_relation(
732 application_name_1
=app_1
,
733 application_name_2
=app_2
,
734 relation_1
=endpoint_1
,
735 relation_2
=endpoint_2
,
737 except Exception as e
:
738 message
= "Error adding relation between {} and {}: {}".format(
741 self
.log
.error(message
)
742 raise N2VCException(message
=message
)
744 async def remove_relation(self
):
746 self
.log
.info("Method not implemented yet")
747 raise MethodNotImplemented()
749 async def deregister_execution_environments(self
):
750 self
.log
.info("Method not implemented yet")
751 raise MethodNotImplemented()
753 async def delete_namespace(
754 self
, namespace
: str, db_dict
: dict = None, total_timeout
: float = None
756 self
.log
.info("Deleting namespace={}".format(namespace
))
759 if namespace
is None:
760 raise N2VCBadArgumentsException(
761 message
="namespace is mandatory", bad_args
=["namespace"]
764 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
767 if ns_id
is not None:
769 models
= await self
.libjuju
.list_models(contains
=ns_id
)
771 await self
.libjuju
.destroy_model(
772 model_name
=model
, total_timeout
=total_timeout
774 except Exception as e
:
776 message
="Error deleting namespace {} : {}".format(namespace
, e
)
779 raise N2VCBadArgumentsException(
780 message
="only ns_id is permitted to delete yet", bad_args
=["namespace"]
783 self
.log
.info("Namespace {} deleted".format(namespace
))
785 async def delete_execution_environment(
786 self
, ee_id
: str, db_dict
: dict = None, total_timeout
: float = None
788 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
792 raise N2VCBadArgumentsException(
793 message
="ee_id is mandatory", bad_args
=["ee_id"]
796 model_name
, application_name
, _machine_id
= self
._get
_ee
_id
_components
(
800 # destroy the application
802 await self
.libjuju
.destroy_model(
803 model_name
=model_name
, total_timeout
=total_timeout
805 except Exception as e
:
808 "Error deleting execution environment {} (application {}) : {}"
809 ).format(ee_id
, application_name
, e
)
812 # destroy the machine
814 # await self._juju_destroy_machine(
815 # model_name=model_name,
816 # machine_id=machine_id,
817 # total_timeout=total_timeout
819 # except Exception as e:
820 # raise N2VCException(
821 # message='Error deleting execution environment {} (machine {}) : {}'
822 # .format(ee_id, machine_id, e))
824 self
.log
.info("Execution environment {} deleted".format(ee_id
))
826 async def exec_primitive(
831 db_dict
: dict = None,
832 progress_timeout
: float = None,
833 total_timeout
: float = None,
837 "Executing primitive: {} on ee: {}, params: {}".format(
838 primitive_name
, ee_id
, params_dict
843 if ee_id
is None or len(ee_id
) == 0:
844 raise N2VCBadArgumentsException(
845 message
="ee_id is mandatory", bad_args
=["ee_id"]
847 if primitive_name
is None or len(primitive_name
) == 0:
848 raise N2VCBadArgumentsException(
849 message
="action_name is mandatory", bad_args
=["action_name"]
851 if params_dict
is None:
859 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
861 raise N2VCBadArgumentsException(
862 message
="ee_id={} is not a valid execution environment id".format(
868 if primitive_name
== "config":
869 # Special case: config primitive
871 await self
.libjuju
.configure_application(
872 model_name
=model_name
,
873 application_name
=application_name
,
876 actions
= await self
.libjuju
.get_actions(
877 application_name
=application_name
, model_name
=model_name
,
880 "Application {} has these actions: {}".format(
881 application_name
, actions
884 if "verify-ssh-credentials" in actions
:
885 # execute verify-credentials
888 for _
in range(num_retries
):
890 self
.log
.debug("Executing action verify-ssh-credentials...")
891 output
, ok
= await self
.libjuju
.execute_action(
892 model_name
=model_name
,
893 application_name
=application_name
,
894 action_name
="verify-ssh-credentials",
896 progress_timeout
=progress_timeout
,
897 total_timeout
=total_timeout
,
902 "Error executing verify-ssh-credentials: {}. Retrying..."
904 await asyncio
.sleep(retry_timeout
)
907 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
909 except asyncio
.CancelledError
:
913 "Error executing verify-ssh-credentials after {} retries. ".format(
918 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
921 self
.log
.debug(msg
=msg
)
922 except Exception as e
:
923 self
.log
.error("Error configuring juju application: {}".format(e
))
924 raise N2VCExecutionException(
925 message
="Error configuring application into ee={} : {}".format(
928 primitive_name
=primitive_name
,
933 output
, status
= await self
.libjuju
.execute_action(
934 model_name
=model_name
,
935 application_name
=application_name
,
936 action_name
=primitive_name
,
938 progress_timeout
=progress_timeout
,
939 total_timeout
=total_timeout
,
942 if status
== "completed":
945 raise Exception("status: {}, output: {}".format(status
, output
))
946 except Exception as e
:
948 "Error executing primitive {}: {}".format(primitive_name
, e
)
950 raise N2VCExecutionException(
951 message
="Error executing primitive {} into ee={} : {}".format(
952 primitive_name
, ee_id
, e
954 primitive_name
=primitive_name
,
957 async def disconnect(self
):
958 self
.log
.info("closing juju N2VC...")
960 await self
.libjuju
.disconnect()
961 except Exception as e
:
962 raise N2VCConnectionException(
963 message
="Error disconnecting controller: {}".format(e
), url
=self
.url
967 ####################################################################################
968 ################################### P R I V A T E ##################################
969 ####################################################################################
972 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
974 # write ee_id to database: _admin.deployed.VCA.x
976 the_table
= db_dict
["collection"]
977 the_filter
= db_dict
["filter"]
978 the_path
= db_dict
["path"]
979 if not the_path
[-1] == ".":
980 the_path
= the_path
+ "."
981 update_dict
= {the_path
+ "ee_id": ee_id
}
982 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
986 update_dict
=update_dict
,
989 except asyncio
.CancelledError
:
991 except Exception as e
:
992 self
.log
.error("Error writing ee_id to database: {}".format(e
))
995 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
997 Build an execution environment id form model, application and machine
999 :param application_name:
1003 # id for the execution environment
1004 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
1007 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
1009 Get model, application and machine components from an execution environment id
1011 :return: model_name, application_name, machine_id
1015 return None, None, None
1017 # split components of id
1018 parts
= ee_id
.split(".")
1019 model_name
= parts
[0]
1020 application_name
= parts
[1]
1021 machine_id
= parts
[2]
1022 return model_name
, application_name
, machine_id
1024 def _get_application_name(self
, namespace
: str) -> str:
1026 Build application name from namespace
1028 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1031 # TODO: Enforce the Juju 50-character application limit
1033 # split namespace components
1034 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
1038 if vnf_id
is None or len(vnf_id
) == 0:
1041 # Shorten the vnf_id to its last twelve characters
1042 vnf_id
= "vnf-" + vnf_id
[-12:]
1044 if vdu_id
is None or len(vdu_id
) == 0:
1047 # Shorten the vdu_id to its last twelve characters
1048 vdu_id
= "-vdu-" + vdu_id
[-12:]
1050 if vdu_count
is None or len(vdu_count
) == 0:
1053 vdu_count
= "-cnt-" + vdu_count
1055 application_name
= "app-{}{}{}".format(vnf_id
, vdu_id
, vdu_count
)
1057 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1059 async def _juju_create_machine(
1062 application_name
: str,
1063 machine_id
: str = None,
1064 db_dict
: dict = None,
1065 progress_timeout
: float = None,
1066 total_timeout
: float = None,
1070 "creating machine in model: {}, existing machine id: {}".format(
1071 model_name
, machine_id
1075 # get juju model and observer (create model if needed)
1076 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1077 observer
= self
.juju_observers
[model_name
]
1079 # find machine id in model
1081 if machine_id
is not None:
1082 self
.log
.debug("Finding existing machine id {} in model".format(machine_id
))
1083 # get juju existing machines in the model
1084 existing_machines
= await model
.get_machines()
1085 if machine_id
in existing_machines
:
1087 "Machine id {} found in model (reusing it)".format(machine_id
)
1089 machine
= model
.machines
[machine_id
]
1092 self
.log
.debug("Creating a new machine in juju...")
1093 # machine does not exist, create it and wait for it
1094 machine
= await model
.add_machine(
1095 spec
=None, constraints
=None, disks
=None, series
="xenial"
1098 # register machine with observer
1099 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1101 # id for the execution environment
1102 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
1103 model_name
=model_name
,
1104 application_name
=application_name
,
1105 machine_id
=str(machine
.entity_id
),
1108 # write ee_id in database
1109 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
1111 # wait for machine creation
1112 await observer
.wait_for_machine(
1113 machine_id
=str(machine
.entity_id
),
1114 progress_timeout
=progress_timeout
,
1115 total_timeout
=total_timeout
,
1120 self
.log
.debug("Reusing old machine pending")
1122 # register machine with observer
1123 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1125 # machine does exist, but it is in creation process (pending), wait for
1126 # create finalisation
1127 await observer
.wait_for_machine(
1128 machine_id
=machine
.entity_id
,
1129 progress_timeout
=progress_timeout
,
1130 total_timeout
=total_timeout
,
1133 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
1136 async def _juju_provision_machine(
1141 private_key_path
: str,
1142 db_dict
: dict = None,
1143 progress_timeout
: float = None,
1144 total_timeout
: float = None,
1147 if not self
.api_proxy
:
1148 msg
= "Cannot provision machine: api_proxy is not defined"
1149 self
.log
.error(msg
=msg
)
1150 raise N2VCException(message
=msg
)
1153 "provisioning machine. model: {}, hostname: {}, username: {}".format(
1154 model_name
, hostname
, username
1158 if not self
._authenticated
:
1159 await self
._juju
_login
()
1161 # get juju model and observer
1162 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1163 observer
= self
.juju_observers
[model_name
]
1165 # TODO check if machine is already provisioned
1166 machine_list
= await model
.get_machines()
1168 provisioner
= AsyncSSHProvisioner(
1171 private_key_path
=private_key_path
,
1177 params
= await provisioner
.provision_machine()
1178 except Exception as ex
:
1179 msg
= "Exception provisioning machine: {}".format(ex
)
1181 raise N2VCException(message
=msg
)
1183 params
.jobs
= ["JobHostUnits"]
1185 connection
= model
.connection()
1187 # Submit the request.
1188 self
.log
.debug("Adding machine to model")
1189 client_facade
= client
.ClientFacade
.from_connection(connection
)
1190 results
= await client_facade
.AddMachines(params
=[params
])
1191 error
= results
.machines
[0].error
1193 msg
= "Error adding machine: {}".format(error
.message
)
1194 self
.log
.error(msg
=msg
)
1195 raise ValueError(msg
)
1197 machine_id
= results
.machines
[0].machine
1199 # Need to run this after AddMachines has been called,
1200 # as we need the machine_id
1201 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
1202 asyncio
.ensure_future(
1203 provisioner
.install_agent(
1204 connection
=connection
,
1206 machine_id
=machine_id
,
1207 proxy
=self
.api_proxy
,
1211 # wait for machine in model (now, machine is not yet in model, so we must
1215 machine_list
= await model
.get_machines()
1216 if machine_id
in machine_list
:
1217 self
.log
.debug("Machine {} found in model!".format(machine_id
))
1218 machine
= model
.machines
.get(machine_id
)
1220 await asyncio
.sleep(2)
1223 msg
= "Machine {} not found in model".format(machine_id
)
1224 self
.log
.error(msg
=msg
)
1225 raise Exception(msg
)
1227 # register machine with observer
1228 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1230 # wait for machine creation
1231 self
.log
.debug("waiting for provision finishes... {}".format(machine_id
))
1232 await observer
.wait_for_machine(
1233 machine_id
=machine_id
,
1234 progress_timeout
=progress_timeout
,
1235 total_timeout
=total_timeout
,
1238 self
.log
.debug("Machine provisioned {}".format(machine_id
))
1242 async def _juju_deploy_charm(
1245 application_name
: str,
1249 progress_timeout
: float = None,
1250 total_timeout
: float = None,
1251 config
: dict = None,
1252 ) -> (Application
, int):
1254 # get juju model and observer
1255 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1256 observer
= self
.juju_observers
[model_name
]
1258 # check if application already exists
1260 if application_name
in model
.applications
:
1261 application
= model
.applications
[application_name
]
1263 if application
is None:
1265 # application does not exist, create it and wait for it
1267 "deploying application {} to machine {}, model {}".format(
1268 application_name
, machine_id
, model_name
1271 self
.log
.debug("charm: {}".format(charm_path
))
1272 machine
= model
.machines
[machine_id
]
1274 application
= await model
.deploy(
1275 entity_url
=charm_path
,
1276 application_name
=application_name
,
1279 series
=machine
.series
,
1284 # register application with observer
1285 observer
.register_application(application
=application
, db_dict
=db_dict
)
1288 "waiting for application deployed... {}".format(application
.entity_id
)
1290 retries
= await observer
.wait_for_application(
1291 application_id
=application
.entity_id
,
1292 progress_timeout
=progress_timeout
,
1293 total_timeout
=total_timeout
,
1295 self
.log
.debug("application deployed")
1299 # register application with observer
1300 observer
.register_application(application
=application
, db_dict
=db_dict
)
1302 # application already exists, but not finalised
1303 self
.log
.debug("application already exists, waiting for deployed...")
1304 retries
= await observer
.wait_for_application(
1305 application_id
=application
.entity_id
,
1306 progress_timeout
=progress_timeout
,
1307 total_timeout
=total_timeout
,
1309 self
.log
.debug("application deployed")
1311 return application
, retries
1313 async def _juju_execute_action(
1316 application_name
: str,
1319 progress_timeout
: float = None,
1320 total_timeout
: float = None,
1324 # get juju model and observer
1325 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1326 observer
= self
.juju_observers
[model_name
]
1328 application
= await self
._juju
_get
_application
(
1329 model_name
=model_name
, application_name
=application_name
1333 for u
in application
.units
:
1334 if await u
.is_leader_from_status():
1336 if unit
is not None:
1337 actions
= await application
.get_actions()
1338 if action_name
in actions
:
1340 'executing action "{}" using params: {}'.format(action_name
, kwargs
)
1342 action
= await unit
.run_action(action_name
, **kwargs
)
1344 # register action with observer
1345 observer
.register_action(action
=action
, db_dict
=db_dict
)
1347 await observer
.wait_for_action(
1348 action_id
=action
.entity_id
,
1349 progress_timeout
=progress_timeout
,
1350 total_timeout
=total_timeout
,
1352 self
.log
.debug("action completed with status: {}".format(action
.status
))
1353 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1354 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1355 if action
.entity_id
in status
:
1356 status
= status
[action
.entity_id
]
1359 return output
, status
1361 raise N2VCExecutionException(
1362 message
="Cannot execute action on charm", primitive_name
=action_name
1365 async def _juju_configure_application(
1368 application_name
: str,
1371 progress_timeout
: float = None,
1372 total_timeout
: float = None,
1375 # get the application
1376 application
= await self
._juju
_get
_application
(
1377 model_name
=model_name
, application_name
=application_name
1381 "configuring the application {} -> {}".format(application_name
, config
)
1383 res
= await application
.set_config(config
)
1385 "application {} configured. res={}".format(application_name
, res
)
1388 # Verify the config is set
1389 new_conf
= await application
.get_config()
1391 value
= new_conf
[key
]["value"]
1392 self
.log
.debug(" {} = {}".format(key
, value
))
1393 if config
[key
] != value
:
1394 raise N2VCException(
1395 message
="key {} is not configured correctly {} != {}".format(
1396 key
, config
[key
], new_conf
[key
]
1400 # check if 'verify-ssh-credentials' action exists
1401 # unit = application.units[0]
1402 actions
= await application
.get_actions()
1403 if "verify-ssh-credentials" not in actions
:
1405 "Action verify-ssh-credentials does not exist in application {}"
1406 ).format(application_name
)
1407 self
.log
.debug(msg
=msg
)
1410 # execute verify-credentials
1412 retry_timeout
= 15.0
1413 for _
in range(num_retries
):
1415 self
.log
.debug("Executing action verify-ssh-credentials...")
1416 output
, ok
= await self
._juju
_execute
_action
(
1417 model_name
=model_name
,
1418 application_name
=application_name
,
1419 action_name
="verify-ssh-credentials",
1421 progress_timeout
=progress_timeout
,
1422 total_timeout
=total_timeout
,
1424 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1426 except asyncio
.CancelledError
:
1428 except Exception as e
:
1430 "Error executing verify-ssh-credentials: {}. Retrying...".format(e
)
1432 await asyncio
.sleep(retry_timeout
)
1435 "Error executing verify-ssh-credentials after {} retries. ".format(
1441 async def _juju_get_application(self
, model_name
: str, application_name
: str):
1442 """Get the deployed application."""
1444 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1446 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1448 if model
.applications
and application_name
in model
.applications
:
1449 return model
.applications
[application_name
]
1451 raise N2VCException(
1452 message
="Cannot get application {} from model {}".format(
1453 application_name
, model_name
1457 async def _juju_get_model(self
, model_name
: str) -> Model
:
1458 """ Get a model object from juju controller
1459 If the model does not exits, it creates it.
1461 :param str model_name: name of the model
1462 :returns Model: model obtained from juju controller or Exception
1466 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1468 if model_name
in self
.juju_models
:
1469 return self
.juju_models
[model_name
]
1471 if self
._creating
_model
:
1472 self
.log
.debug("Another coroutine is creating a model. Wait...")
1473 while self
._creating
_model
:
1474 # another coroutine is creating a model, wait
1475 await asyncio
.sleep(0.1)
1476 # retry (perhaps another coroutine has created the model meanwhile)
1477 if model_name
in self
.juju_models
:
1478 return self
.juju_models
[model_name
]
1481 self
._creating
_model
= True
1483 # get juju model names from juju
1484 model_list
= await self
.controller
.list_models()
1485 if model_name
not in model_list
:
1487 "Model {} does not exist. Creating new model...".format(model_name
)
1489 config_dict
= {"authorized-keys": self
.public_key
}
1491 config_dict
["apt-mirror"] = self
.apt_mirror
1492 if not self
.enable_os_upgrade
:
1493 config_dict
["enable-os-refresh-update"] = False
1494 config_dict
["enable-os-upgrade"] = False
1495 if self
.cloud
in self
.BUILT_IN_CLOUDS
:
1496 model
= await self
.controller
.add_model(
1497 model_name
=model_name
,
1499 cloud_name
=self
.cloud
,
1502 model
= await self
.controller
.add_model(
1503 model_name
=model_name
,
1505 cloud_name
=self
.cloud
,
1506 credential_name
=self
.cloud
,
1508 self
.log
.info("New model created, name={}".format(model_name
))
1511 "Model already exists in juju. Getting model {}".format(model_name
)
1513 model
= await self
.controller
.get_model(model_name
)
1514 self
.log
.debug("Existing model in juju, name={}".format(model_name
))
1516 self
.juju_models
[model_name
] = model
1517 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1520 except Exception as e
:
1521 msg
= "Cannot get model {}. Exception: {}".format(model_name
, e
)
1523 raise N2VCException(msg
)
1525 self
._creating
_model
= False
1527 async def _juju_add_relation(
1530 application_name_1
: str,
1531 application_name_2
: str,
1536 # get juju model and observer
1537 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1539 r1
= "{}:{}".format(application_name_1
, relation_1
)
1540 r2
= "{}:{}".format(application_name_2
, relation_2
)
1542 self
.log
.debug("adding relation: {} -> {}".format(r1
, r2
))
1544 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1545 except JujuAPIError
as e
:
1546 # If one of the applications in the relationship doesn't exist, or the
1547 # relation has already been added,
1548 # let the operation fail silently.
1549 if "not found" in e
.message
:
1551 if "already exists" in e
.message
:
1553 # another execption, raise it
1556 async def _juju_destroy_application(self
, model_name
: str, application_name
: str):
1559 "Destroying application {} in model {}".format(application_name
, model_name
)
1562 # get juju model and observer
1563 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1564 observer
= self
.juju_observers
[model_name
]
1566 application
= model
.applications
.get(application_name
)
1568 observer
.unregister_application(application_name
)
1569 await application
.destroy()
1571 self
.log
.debug("Application not found: {}".format(application_name
))
1573 async def _juju_destroy_machine(
1574 self
, model_name
: str, machine_id
: str, total_timeout
: float = None
1578 "Destroying machine {} in model {}".format(machine_id
, model_name
)
1581 if total_timeout
is None:
1582 total_timeout
= 3600
1584 # get juju model and observer
1585 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1586 observer
= self
.juju_observers
[model_name
]
1588 machines
= await model
.get_machines()
1589 if machine_id
in machines
:
1590 machine
= model
.machines
[machine_id
]
1591 observer
.unregister_machine(machine_id
)
1592 # TODO: change this by machine.is_manual when this is upstreamed:
1593 # https://github.com/juju/python-libjuju/pull/396
1594 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1596 ].startswith("manual:"):
1597 self
.log
.debug("machine.destroy(force=True) started.")
1598 await machine
.destroy(force
=True)
1599 self
.log
.debug("machine.destroy(force=True) passed.")
1601 end
= time
.time() + total_timeout
1602 # wait for machine removal
1603 machines
= await model
.get_machines()
1604 while machine_id
in machines
and time
.time() < end
:
1606 "Waiting for machine {} is destroyed".format(machine_id
)
1608 await asyncio
.sleep(0.5)
1609 machines
= await model
.get_machines()
1610 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1612 self
.log
.debug("Machine not found: {}".format(machine_id
))
1614 async def _juju_destroy_model(self
, model_name
: str, total_timeout
: float = None):
1616 self
.log
.debug("Destroying model {}".format(model_name
))
1618 if total_timeout
is None:
1619 total_timeout
= 3600
1620 end
= time
.time() + total_timeout
1622 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1625 raise N2VCNotFound(message
="Model {} does not exist".format(model_name
))
1627 uuid
= model
.info
.uuid
1629 # destroy applications
1630 for application_name
in model
.applications
:
1632 await self
._juju
_destroy
_application
(
1633 model_name
=model_name
, application_name
=application_name
1635 except Exception as e
:
1637 "Error destroying application {} in model {}: {}".format(
1638 application_name
, model_name
, e
1643 machines
= await model
.get_machines()
1644 for machine_id
in machines
:
1646 await self
._juju
_destroy
_machine
(
1647 model_name
=model_name
, machine_id
=machine_id
1649 except asyncio
.CancelledError
:
1652 # ignore exceptions destroying machine
1655 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1657 self
.log
.debug("destroying model {}...".format(model_name
))
1658 await self
.controller
.destroy_model(uuid
)
1659 # self.log.debug('model destroy requested {}'.format(model_name))
1661 # wait for model is completely destroyed
1662 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
1664 while time
.time() < end
:
1666 # await self.controller.get_model(uuid)
1667 models
= await self
.controller
.list_models()
1668 if model_name
not in models
:
1670 "The model {} ({}) was destroyed".format(model_name
, uuid
)
1673 except asyncio
.CancelledError
:
1675 except Exception as e
:
1677 await asyncio
.sleep(5)
1678 raise N2VCException(
1679 "Timeout waiting for model {} to be destroyed {}".format(
1680 model_name
, last_exception
1684 async def _juju_login(self
):
1685 """Connect to juju controller
1689 # if already authenticated, exit function
1690 if self
._authenticated
:
1693 # if connecting, wait for finish
1694 # another task could be trying to connect in parallel
1695 while self
._connecting
:
1696 await asyncio
.sleep(0.1)
1698 # double check after other task has finished
1699 if self
._authenticated
:
1703 self
._connecting
= True
1705 "connecting to juju controller: {} {}:{}{}".format(
1708 self
.secret
[:8] + "...",
1709 " with ca_cert" if self
.ca_cert
else "",
1713 # Create controller object
1714 self
.controller
= Controller(loop
=self
.loop
)
1715 # Connect to controller
1716 await self
.controller
.connect(
1718 username
=self
.username
,
1719 password
=self
.secret
,
1720 cacert
=self
.ca_cert
,
1722 self
._authenticated
= True
1723 self
.log
.info("juju controller connected")
1724 except Exception as e
:
1725 message
= "Exception connecting to juju: {}".format(e
)
1726 self
.log
.error(message
)
1727 raise N2VCConnectionException(message
=message
, url
=self
.url
)
1729 self
._connecting
= False
1731 async def _juju_logout(self
):
1732 """Logout of the Juju controller."""
1733 if not self
._authenticated
:
1736 # disconnect all models
1737 for model_name
in self
.juju_models
:
1739 await self
._juju
_disconnect
_model
(model_name
)
1740 except Exception as e
:
1742 "Error disconnecting model {} : {}".format(model_name
, e
)
1744 # continue with next model...
1746 self
.log
.info("Disconnecting controller")
1748 await self
.controller
.disconnect()
1749 except Exception as e
:
1750 raise N2VCConnectionException(
1751 message
="Error disconnecting controller: {}".format(e
), url
=self
.url
1754 self
.controller
= None
1755 self
._authenticated
= False
1756 self
.log
.info("disconnected")
1758 async def _juju_disconnect_model(self
, model_name
: str):
1759 self
.log
.debug("Disconnecting model {}".format(model_name
))
1760 if model_name
in self
.juju_models
:
1761 await self
.juju_models
[model_name
].disconnect()
1762 self
.juju_models
[model_name
] = None
1763 self
.juju_observers
[model_name
] = None
1765 self
.warning("Cannot disconnect model: {}".format(model_name
))
1767 def _create_juju_public_key(self
):
1768 """Recreate the Juju public key on lcm container, if needed
1769 Certain libjuju commands expect to be run from the same machine as Juju
1770 is bootstrapped to. This method will write the public key to disk in
1771 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1774 # Make sure that we have a public key before writing to disk
1775 if self
.public_key
is None or len(self
.public_key
) == 0:
1776 if "OSMLCM_VCA_PUBKEY" in os
.environ
:
1777 self
.public_key
= os
.getenv("OSMLCM_VCA_PUBKEY", "")
1778 if len(self
.public_key
) == 0:
1783 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser("~"))
1784 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1786 "writing juju public key to file:\n{}\npublic key: {}".format(
1787 file_path
, self
.public_key
1790 if not os
.path
.exists(pk_path
):
1791 # create path and write file
1792 os
.makedirs(pk_path
)
1793 with
open(file_path
, "w") as f
:
1794 self
.log
.debug("Creating juju public key file: {}".format(file_path
))
1795 f
.write(self
.public_key
)
1797 self
.log
.debug("juju public key file already exists: {}".format(file_path
))
1800 def _format_model_name(name
: str) -> str:
1801 """Format the name of the model.
1803 Model names may only contain lowercase letters, digits and hyphens
1806 return name
.replace("_", "-").replace(" ", "-").lower()
1809 def _format_app_name(name
: str) -> str:
1810 """Format the name of the application (in order to assure valid application name).
1812 Application names have restrictions (run juju deploy --help):
1813 - contains lowercase letters 'a'-'z'
1814 - contains numbers '0'-'9'
1815 - contains hyphens '-'
1816 - starts with a lowercase letter
1817 - not two or more consecutive hyphens
1818 - after a hyphen, not a group with all numbers
1821 def all_numbers(s
: str) -> bool:
1827 new_name
= name
.replace("_", "-")
1828 new_name
= new_name
.replace(" ", "-")
1829 new_name
= new_name
.lower()
1830 while new_name
.find("--") >= 0:
1831 new_name
= new_name
.replace("--", "-")
1832 groups
= new_name
.split("-")
1834 # find 'all numbers' groups and prefix them with a letter
1836 for i
in range(len(groups
)):
1838 if all_numbers(group
):
1844 if app_name
[0].isdigit():
1845 app_name
= "z" + app_name