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
:
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. Support for native charms is disabled"
179 if "enable_os_upgrade" in vca_config
:
180 self
.enable_os_upgrade
= vca_config
["enable_os_upgrade"]
182 self
.enable_os_upgrade
= True
184 if "apt_mirror" in vca_config
:
185 self
.apt_mirror
= vca_config
["apt_mirror"]
187 self
.apt_mirror
= None
189 self
.cloud
= vca_config
.get('cloud')
190 self
.k8s_cloud
= None
191 if "k8s_cloud" in vca_config
:
192 self
.k8s_cloud
= vca_config
.get("k8s_cloud")
193 self
.log
.debug('Arguments have been checked')
196 self
.controller
= None # it will be filled when connect to juju
197 self
.juju_models
= {} # model objects for every model_name
198 self
.juju_observers
= {} # model observers for every model_name
200 False # while connecting to juju (to avoid duplicate connections)
202 self
._authenticated
= (
203 False # it will be True when juju connection be stablished
205 self
._creating
_model
= False # True during model creation
206 self
.libjuju
= Libjuju(
208 api_proxy
=self
.api_proxy
,
209 enable_os_upgrade
=self
.enable_os_upgrade
,
210 apt_mirror
=self
.apt_mirror
,
211 username
=self
.username
,
212 password
=self
.secret
,
220 # create juju pub key file in lcm container at
221 # ./local/share/juju/ssh/juju_id_rsa.pub
222 self
._create
_juju
_public
_key
()
224 self
.log
.info("N2VC juju connector initialized")
226 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
228 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
230 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
233 # model name is ns_id
235 if model_name
is None:
236 msg
= "Namespace {} not valid".format(namespace
)
238 raise N2VCBadArgumentsException(msg
, ["namespace"])
241 models
= await self
.libjuju
.list_models(contains
=ns_id
)
244 status
[m
] = self
.libjuju
.get_model_status(m
)
247 return obj_to_yaml(status
)
249 return obj_to_dict(status
)
251 async def create_execution_environment(
255 reuse_ee_id
: str = None,
256 progress_timeout
: float = None,
257 total_timeout
: float = None,
261 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
262 namespace
, reuse_ee_id
268 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
278 ) = self
._get
_namespace
_components
(namespace
=namespace
)
279 # model name is ns_id
282 application_name
= self
._get
_application
_name
(namespace
=namespace
)
285 "model name: {}, application name: {}, machine_id: {}".format(
286 model_name
, application_name
, machine_id
290 # create or reuse a new juju machine
292 if not await self
.libjuju
.model_exists(model_name
):
293 await self
.libjuju
.add_model(model_name
, cloud_name
=self
.cloud
)
294 machine
, new
= await self
.libjuju
.create_machine(
295 model_name
=model_name
,
296 machine_id
=machine_id
,
298 progress_timeout
=progress_timeout
,
299 total_timeout
=total_timeout
,
301 # id for the execution environment
302 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
303 model_name
=model_name
,
304 application_name
=application_name
,
305 machine_id
=str(machine
.entity_id
),
307 self
.log
.debug("ee_id: {}".format(ee_id
))
310 # write ee_id in database
311 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
313 except Exception as e
:
314 message
= "Error creating machine on juju: {}".format(e
)
315 self
.log
.error(message
)
316 raise N2VCException(message
=message
)
318 # new machine credentials
320 "hostname": machine
.dns_name
,
324 "Execution environment created. ee_id: {}, credentials: {}".format(
329 return ee_id
, credentials
331 async def register_execution_environment(
336 progress_timeout
: float = None,
337 total_timeout
: float = None,
341 "Registering execution environment. namespace={}, credentials={}".format(
342 namespace
, credentials
346 if credentials
is None:
347 raise N2VCBadArgumentsException(
348 message
="credentials are mandatory", bad_args
=["credentials"]
350 if credentials
.get("hostname"):
351 hostname
= credentials
["hostname"]
353 raise N2VCBadArgumentsException(
354 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
356 if credentials
.get("username"):
357 username
= credentials
["username"]
359 raise N2VCBadArgumentsException(
360 message
="username is mandatory", bad_args
=["credentials.username"]
362 if "private_key_path" in credentials
:
363 private_key_path
= credentials
["private_key_path"]
365 # if not passed as argument, use generated private key path
366 private_key_path
= self
.private_key_path
368 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
375 application_name
= self
._get
_application
_name
(namespace
=namespace
)
377 # register machine on juju
379 if not self
.api_proxy
:
380 msg
= "Cannot provision machine: api_proxy is not defined"
381 self
.log
.error(msg
=msg
)
382 raise N2VCException(message
=msg
)
383 if not await self
.libjuju
.model_exists(model_name
):
384 await self
.libjuju
.add_model(model_name
, cloud_name
=self
.cloud
)
385 machine_id
= await self
.libjuju
.provision_machine(
386 model_name
=model_name
,
389 private_key_path
=private_key_path
,
391 progress_timeout
=progress_timeout
,
392 total_timeout
=total_timeout
,
394 except Exception as e
:
395 self
.log
.error("Error registering machine: {}".format(e
))
397 message
="Error registering machine on juju: {}".format(e
)
400 self
.log
.info("Machine registered: {}".format(machine_id
))
402 # id for the execution environment
403 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
404 model_name
=model_name
,
405 application_name
=application_name
,
406 machine_id
=str(machine_id
),
409 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
413 async def install_configuration_sw(
418 progress_timeout
: float = None,
419 total_timeout
: float = None,
426 "Installing configuration sw on ee_id: {}, "
427 "artifact path: {}, db_dict: {}"
428 ).format(ee_id
, artifact_path
, db_dict
)
432 if ee_id
is None or len(ee_id
) == 0:
433 raise N2VCBadArgumentsException(
434 message
="ee_id is mandatory", bad_args
=["ee_id"]
436 if artifact_path
is None or len(artifact_path
) == 0:
437 raise N2VCBadArgumentsException(
438 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
441 raise N2VCBadArgumentsException(
442 message
="db_dict is mandatory", bad_args
=["db_dict"]
450 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
452 "model: {}, application: {}, machine: {}".format(
453 model_name
, application_name
, machine_id
457 raise N2VCBadArgumentsException(
458 message
="ee_id={} is not a valid execution environment id".format(
464 # remove // in charm path
465 while artifact_path
.find("//") >= 0:
466 artifact_path
= artifact_path
.replace("//", "/")
469 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
470 msg
= "artifact path does not exist: {}".format(artifact_path
)
471 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
473 if artifact_path
.startswith("/"):
474 full_path
= self
.fs
.path
+ artifact_path
476 full_path
= self
.fs
.path
+ "/" + artifact_path
479 await self
.libjuju
.deploy_charm(
480 model_name
=model_name
,
481 application_name
=application_name
,
483 machine_id
=machine_id
,
485 progress_timeout
=progress_timeout
,
486 total_timeout
=total_timeout
,
490 except Exception as e
:
492 message
="Error desploying charm into ee={} : {}".format(ee_id
, e
)
495 self
.log
.info("Configuration sw installed")
497 async def install_k8s_proxy_charm(
503 progress_timeout
: float = None,
504 total_timeout
: float = None,
508 Install a k8s proxy charm
510 :param charm_name: Name of the charm being deployed
511 :param namespace: collection of all the uuids related to the charm.
512 :param str artifact_path: where to locate the artifacts (parent folder) using
514 the final artifact path will be a combination of this artifact_path and
515 additional string from the config_dict (e.g. charm name)
516 :param dict db_dict: where to write into database when the status changes.
517 It contains a dict with
518 {collection: <str>, filter: {}, path: <str>},
519 e.g. {collection: "nsrs", filter:
520 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
521 :param float progress_timeout:
522 :param float total_timeout:
523 :param config: Dictionary with additional configuration
525 :returns ee_id: execution environment id.
527 self
.log
.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
528 .format(charm_name
, artifact_path
, db_dict
))
530 if not self
.k8s_cloud
:
531 raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
533 if artifact_path
is None or len(artifact_path
) == 0:
534 raise N2VCBadArgumentsException(
535 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
538 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
540 # remove // in charm path
541 while artifact_path
.find('//') >= 0:
542 artifact_path
= artifact_path
.replace('//', '/')
545 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
546 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
547 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
549 if artifact_path
.startswith('/'):
550 full_path
= self
.fs
.path
+ artifact_path
552 full_path
= self
.fs
.path
+ '/' + artifact_path
554 _
, ns_id
, _
, _
, _
= self
._get
_namespace
_components
(namespace
=namespace
)
555 model_name
= '{}-k8s'.format(ns_id
)
557 await self
.libjuju
.add_model(model_name
, self
.k8s_cloud
)
558 application_name
= self
._get
_application
_name
(namespace
)
561 await self
.libjuju
.deploy_charm(
562 model_name
=model_name
,
563 application_name
=application_name
,
567 progress_timeout
=progress_timeout
,
568 total_timeout
=total_timeout
,
571 except Exception as e
:
572 raise N2VCException(message
='Error deploying charm: {}'.format(e
))
574 self
.log
.info('K8s proxy charm installed')
575 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
576 model_name
=model_name
,
577 application_name
=application_name
,
581 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
585 async def get_ee_ssh_public__key(
589 progress_timeout
: float = None,
590 total_timeout
: float = None,
595 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
596 ).format(ee_id
, db_dict
)
600 if ee_id
is None or len(ee_id
) == 0:
601 raise N2VCBadArgumentsException(
602 message
="ee_id is mandatory", bad_args
=["ee_id"]
605 raise N2VCBadArgumentsException(
606 message
="db_dict is mandatory", bad_args
=["db_dict"]
614 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
616 "model: {}, application: {}, machine: {}".format(
617 model_name
, application_name
, machine_id
621 raise N2VCBadArgumentsException(
622 message
="ee_id={} is not a valid execution environment id".format(
628 # try to execute ssh layer primitives (if exist):
634 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
636 # execute action: generate-ssh-key
638 output
, _status
= await self
.libjuju
.execute_action(
639 model_name
=model_name
,
640 application_name
=application_name
,
641 action_name
="generate-ssh-key",
643 progress_timeout
=progress_timeout
,
644 total_timeout
=total_timeout
,
646 except Exception as e
:
648 "Skipping exception while executing action generate-ssh-key: {}".format(
653 # execute action: get-ssh-public-key
655 output
, _status
= await self
.libjuju
.execute_action(
656 model_name
=model_name
,
657 application_name
=application_name
,
658 action_name
="get-ssh-public-key",
660 progress_timeout
=progress_timeout
,
661 total_timeout
=total_timeout
,
663 except Exception as e
:
664 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
666 raise N2VCExecutionException(e
, primitive_name
="get-ssh-public-key")
668 # return public key if exists
669 return output
["pubkey"] if "pubkey" in output
else output
671 async def add_relation(
672 self
, ee_id_1
: str, ee_id_2
: str, endpoint_1
: str, endpoint_2
: str
676 "adding new relation between {} and {}, endpoints: {}, {}".format(
677 ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
683 message
= "EE 1 is mandatory"
684 self
.log
.error(message
)
685 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_1"])
687 message
= "EE 2 is mandatory"
688 self
.log
.error(message
)
689 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_2"])
691 message
= "endpoint 1 is mandatory"
692 self
.log
.error(message
)
693 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_1"])
695 message
= "endpoint 2 is mandatory"
696 self
.log
.error(message
)
697 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_2"])
699 # get the model, the applications and the machines from the ee_id's
700 model_1
, app_1
, _machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
701 model_2
, app_2
, _machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
703 # model must be the same
704 if model_1
!= model_2
:
705 message
= "EE models are not the same: {} vs {}".format(ee_id_1
, ee_id_2
)
706 self
.log
.error(message
)
707 raise N2VCBadArgumentsException(
708 message
=message
, bad_args
=["ee_id_1", "ee_id_2"]
711 # add juju relations between two applications
713 await self
.libjuju
.add_relation(
715 application_name_1
=app_1
,
716 application_name_2
=app_2
,
717 relation_1
=endpoint_1
,
718 relation_2
=endpoint_2
,
720 except Exception as e
:
721 message
= "Error adding relation between {} and {}: {}".format(
724 self
.log
.error(message
)
725 raise N2VCException(message
=message
)
727 async def remove_relation(self
):
729 self
.log
.info("Method not implemented yet")
730 raise MethodNotImplemented()
732 async def deregister_execution_environments(self
):
733 self
.log
.info("Method not implemented yet")
734 raise MethodNotImplemented()
736 async def delete_namespace(
737 self
, namespace
: str, db_dict
: dict = None, total_timeout
: float = None
739 self
.log
.info("Deleting namespace={}".format(namespace
))
742 if namespace
is None:
743 raise N2VCBadArgumentsException(
744 message
="namespace is mandatory", bad_args
=["namespace"]
747 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
750 if ns_id
is not None:
752 models
= await self
.libjuju
.list_models(contains
=ns_id
)
754 await self
.libjuju
.destroy_model(
755 model_name
=model
, total_timeout
=total_timeout
757 except Exception as e
:
759 message
="Error deleting namespace {} : {}".format(namespace
, e
)
762 raise N2VCBadArgumentsException(
763 message
="only ns_id is permitted to delete yet", bad_args
=["namespace"]
766 self
.log
.info("Namespace {} deleted".format(namespace
))
768 async def delete_execution_environment(
769 self
, ee_id
: str, db_dict
: dict = None, total_timeout
: float = None
771 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
775 raise N2VCBadArgumentsException(
776 message
="ee_id is mandatory", bad_args
=["ee_id"]
779 model_name
, application_name
, _machine_id
= self
._get
_ee
_id
_components
(
783 # destroy the application
785 await self
.libjuju
.destroy_model(
786 model_name
=model_name
, total_timeout
=total_timeout
788 except Exception as e
:
791 "Error deleting execution environment {} (application {}) : {}"
792 ).format(ee_id
, application_name
, e
)
795 # destroy the machine
797 # await self._juju_destroy_machine(
798 # model_name=model_name,
799 # machine_id=machine_id,
800 # total_timeout=total_timeout
802 # except Exception as e:
803 # raise N2VCException(
804 # message='Error deleting execution environment {} (machine {}) : {}'
805 # .format(ee_id, machine_id, e))
807 self
.log
.info("Execution environment {} deleted".format(ee_id
))
809 async def exec_primitive(
814 db_dict
: dict = None,
815 progress_timeout
: float = None,
816 total_timeout
: float = None,
820 "Executing primitive: {} on ee: {}, params: {}".format(
821 primitive_name
, ee_id
, params_dict
826 if ee_id
is None or len(ee_id
) == 0:
827 raise N2VCBadArgumentsException(
828 message
="ee_id is mandatory", bad_args
=["ee_id"]
830 if primitive_name
is None or len(primitive_name
) == 0:
831 raise N2VCBadArgumentsException(
832 message
="action_name is mandatory", bad_args
=["action_name"]
834 if params_dict
is None:
842 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
844 raise N2VCBadArgumentsException(
845 message
="ee_id={} is not a valid execution environment id".format(
851 if primitive_name
== "config":
852 # Special case: config primitive
854 await self
.libjuju
.configure_application(
855 model_name
=model_name
,
856 application_name
=application_name
,
859 actions
= await self
.libjuju
.get_actions(
860 application_name
=application_name
, model_name
=model_name
,
863 "Application {} has these actions: {}".format(
864 application_name
, actions
867 if "verify-ssh-credentials" in actions
:
868 # execute verify-credentials
871 for _
in range(num_retries
):
873 self
.log
.debug("Executing action verify-ssh-credentials...")
874 output
, ok
= await self
.libjuju
.execute_action(
875 model_name
=model_name
,
876 application_name
=application_name
,
877 action_name
="verify-ssh-credentials",
879 progress_timeout
=progress_timeout
,
880 total_timeout
=total_timeout
,
885 "Error executing verify-ssh-credentials: {}. Retrying..."
887 await asyncio
.sleep(retry_timeout
)
890 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
892 except asyncio
.CancelledError
:
896 "Error executing verify-ssh-credentials after {} retries. ".format(
901 msg
= "Action verify-ssh-credentials does not exist in application {}".format(
904 self
.log
.debug(msg
=msg
)
905 except Exception as e
:
906 self
.log
.error("Error configuring juju application: {}".format(e
))
907 raise N2VCExecutionException(
908 message
="Error configuring application into ee={} : {}".format(
911 primitive_name
=primitive_name
,
916 output
, status
= await self
.libjuju
.execute_action(
917 model_name
=model_name
,
918 application_name
=application_name
,
919 action_name
=primitive_name
,
921 progress_timeout
=progress_timeout
,
922 total_timeout
=total_timeout
,
925 if status
== "completed":
928 raise Exception("status is not completed: {}".format(status
))
929 except Exception as e
:
931 "Error executing primitive {}: {}".format(primitive_name
, e
)
933 raise N2VCExecutionException(
934 message
="Error executing primitive {} into ee={} : {}".format(
935 primitive_name
, ee_id
, e
937 primitive_name
=primitive_name
,
940 async def disconnect(self
):
941 self
.log
.info("closing juju N2VC...")
943 await self
.libjuju
.disconnect()
944 except Exception as e
:
945 raise N2VCConnectionException(
946 message
="Error disconnecting controller: {}".format(e
), url
=self
.url
950 ####################################################################################
951 ################################### P R I V A T E ##################################
952 ####################################################################################
955 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
957 # write ee_id to database: _admin.deployed.VCA.x
959 the_table
= db_dict
["collection"]
960 the_filter
= db_dict
["filter"]
961 the_path
= db_dict
["path"]
962 if not the_path
[-1] == ".":
963 the_path
= the_path
+ "."
964 update_dict
= {the_path
+ "ee_id": ee_id
}
965 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
969 update_dict
=update_dict
,
972 except asyncio
.CancelledError
:
974 except Exception as e
:
975 self
.log
.error("Error writing ee_id to database: {}".format(e
))
978 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
980 Build an execution environment id form model, application and machine
982 :param application_name:
986 # id for the execution environment
987 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
990 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
992 Get model, application and machine components from an execution environment id
994 :return: model_name, application_name, machine_id
998 return None, None, None
1000 # split components of id
1001 parts
= ee_id
.split(".")
1002 model_name
= parts
[0]
1003 application_name
= parts
[1]
1004 machine_id
= parts
[2]
1005 return model_name
, application_name
, machine_id
1007 def _get_application_name(self
, namespace
: str) -> str:
1009 Build application name from namespace
1011 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1014 # TODO: Enforce the Juju 50-character application limit
1016 # split namespace components
1017 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
1021 if vnf_id
is None or len(vnf_id
) == 0:
1024 # Shorten the vnf_id to its last twelve characters
1025 vnf_id
= "vnf-" + vnf_id
[-12:]
1027 if vdu_id
is None or len(vdu_id
) == 0:
1030 # Shorten the vdu_id to its last twelve characters
1031 vdu_id
= "-vdu-" + vdu_id
[-12:]
1033 if vdu_count
is None or len(vdu_count
) == 0:
1036 vdu_count
= "-cnt-" + vdu_count
1038 application_name
= "app-{}{}{}".format(vnf_id
, vdu_id
, vdu_count
)
1040 return N2VCJujuConnector
._format
_app
_name
(application_name
)
1042 async def _juju_create_machine(
1045 application_name
: str,
1046 machine_id
: str = None,
1047 db_dict
: dict = None,
1048 progress_timeout
: float = None,
1049 total_timeout
: float = None,
1053 "creating machine in model: {}, existing machine id: {}".format(
1054 model_name
, machine_id
1058 # get juju model and observer (create model if needed)
1059 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1060 observer
= self
.juju_observers
[model_name
]
1062 # find machine id in model
1064 if machine_id
is not None:
1065 self
.log
.debug("Finding existing machine id {} in model".format(machine_id
))
1066 # get juju existing machines in the model
1067 existing_machines
= await model
.get_machines()
1068 if machine_id
in existing_machines
:
1070 "Machine id {} found in model (reusing it)".format(machine_id
)
1072 machine
= model
.machines
[machine_id
]
1075 self
.log
.debug("Creating a new machine in juju...")
1076 # machine does not exist, create it and wait for it
1077 machine
= await model
.add_machine(
1078 spec
=None, constraints
=None, disks
=None, series
="xenial"
1081 # register machine with observer
1082 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1084 # id for the execution environment
1085 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
1086 model_name
=model_name
,
1087 application_name
=application_name
,
1088 machine_id
=str(machine
.entity_id
),
1091 # write ee_id in database
1092 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
1094 # wait for machine creation
1095 await observer
.wait_for_machine(
1096 machine_id
=str(machine
.entity_id
),
1097 progress_timeout
=progress_timeout
,
1098 total_timeout
=total_timeout
,
1103 self
.log
.debug("Reusing old machine pending")
1105 # register machine with observer
1106 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1108 # machine does exist, but it is in creation process (pending), wait for
1109 # create finalisation
1110 await observer
.wait_for_machine(
1111 machine_id
=machine
.entity_id
,
1112 progress_timeout
=progress_timeout
,
1113 total_timeout
=total_timeout
,
1116 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
1119 async def _juju_provision_machine(
1124 private_key_path
: str,
1125 db_dict
: dict = None,
1126 progress_timeout
: float = None,
1127 total_timeout
: float = None,
1130 if not self
.api_proxy
:
1131 msg
= "Cannot provision machine: api_proxy is not defined"
1132 self
.log
.error(msg
=msg
)
1133 raise N2VCException(message
=msg
)
1136 "provisioning machine. model: {}, hostname: {}, username: {}".format(
1137 model_name
, hostname
, username
1141 if not self
._authenticated
:
1142 await self
._juju
_login
()
1144 # get juju model and observer
1145 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1146 observer
= self
.juju_observers
[model_name
]
1148 # TODO check if machine is already provisioned
1149 machine_list
= await model
.get_machines()
1151 provisioner
= AsyncSSHProvisioner(
1154 private_key_path
=private_key_path
,
1160 params
= await provisioner
.provision_machine()
1161 except Exception as ex
:
1162 msg
= "Exception provisioning machine: {}".format(ex
)
1164 raise N2VCException(message
=msg
)
1166 params
.jobs
= ["JobHostUnits"]
1168 connection
= model
.connection()
1170 # Submit the request.
1171 self
.log
.debug("Adding machine to model")
1172 client_facade
= client
.ClientFacade
.from_connection(connection
)
1173 results
= await client_facade
.AddMachines(params
=[params
])
1174 error
= results
.machines
[0].error
1176 msg
= "Error adding machine: {}".format(error
.message
)
1177 self
.log
.error(msg
=msg
)
1178 raise ValueError(msg
)
1180 machine_id
= results
.machines
[0].machine
1182 # Need to run this after AddMachines has been called,
1183 # as we need the machine_id
1184 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
1185 asyncio
.ensure_future(
1186 provisioner
.install_agent(
1187 connection
=connection
,
1189 machine_id
=machine_id
,
1194 # wait for machine in model (now, machine is not yet in model, so we must
1198 machine_list
= await model
.get_machines()
1199 if machine_id
in machine_list
:
1200 self
.log
.debug("Machine {} found in model!".format(machine_id
))
1201 machine
= model
.machines
.get(machine_id
)
1203 await asyncio
.sleep(2)
1206 msg
= "Machine {} not found in model".format(machine_id
)
1207 self
.log
.error(msg
=msg
)
1208 raise Exception(msg
)
1210 # register machine with observer
1211 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1213 # wait for machine creation
1214 self
.log
.debug("waiting for provision finishes... {}".format(machine_id
))
1215 await observer
.wait_for_machine(
1216 machine_id
=machine_id
,
1217 progress_timeout
=progress_timeout
,
1218 total_timeout
=total_timeout
,
1221 self
.log
.debug("Machine provisioned {}".format(machine_id
))
1225 async def _juju_deploy_charm(
1228 application_name
: str,
1232 progress_timeout
: float = None,
1233 total_timeout
: float = None,
1234 config
: dict = None,
1235 ) -> (Application
, int):
1237 # get juju model and observer
1238 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1239 observer
= self
.juju_observers
[model_name
]
1241 # check if application already exists
1243 if application_name
in model
.applications
:
1244 application
= model
.applications
[application_name
]
1246 if application
is None:
1248 # application does not exist, create it and wait for it
1250 "deploying application {} to machine {}, model {}".format(
1251 application_name
, machine_id
, model_name
1254 self
.log
.debug("charm: {}".format(charm_path
))
1255 machine
= model
.machines
[machine_id
]
1257 application
= await model
.deploy(
1258 entity_url
=charm_path
,
1259 application_name
=application_name
,
1262 series
=machine
.series
,
1267 # register application with observer
1268 observer
.register_application(application
=application
, db_dict
=db_dict
)
1271 "waiting for application deployed... {}".format(application
.entity_id
)
1273 retries
= await observer
.wait_for_application(
1274 application_id
=application
.entity_id
,
1275 progress_timeout
=progress_timeout
,
1276 total_timeout
=total_timeout
,
1278 self
.log
.debug("application deployed")
1282 # register application with observer
1283 observer
.register_application(application
=application
, db_dict
=db_dict
)
1285 # application already exists, but not finalised
1286 self
.log
.debug("application already exists, waiting for deployed...")
1287 retries
= await observer
.wait_for_application(
1288 application_id
=application
.entity_id
,
1289 progress_timeout
=progress_timeout
,
1290 total_timeout
=total_timeout
,
1292 self
.log
.debug("application deployed")
1294 return application
, retries
1296 async def _juju_execute_action(
1299 application_name
: str,
1302 progress_timeout
: float = None,
1303 total_timeout
: float = None,
1307 # get juju model and observer
1308 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1309 observer
= self
.juju_observers
[model_name
]
1311 application
= await self
._juju
_get
_application
(
1312 model_name
=model_name
, application_name
=application_name
1316 for u
in application
.units
:
1317 if await u
.is_leader_from_status():
1319 if unit
is not None:
1320 actions
= await application
.get_actions()
1321 if action_name
in actions
:
1323 'executing action "{}" using params: {}'.format(action_name
, kwargs
)
1325 action
= await unit
.run_action(action_name
, **kwargs
)
1327 # register action with observer
1328 observer
.register_action(action
=action
, db_dict
=db_dict
)
1330 await observer
.wait_for_action(
1331 action_id
=action
.entity_id
,
1332 progress_timeout
=progress_timeout
,
1333 total_timeout
=total_timeout
,
1335 self
.log
.debug("action completed with status: {}".format(action
.status
))
1336 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1337 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1338 if action
.entity_id
in status
:
1339 status
= status
[action
.entity_id
]
1342 return output
, status
1344 raise N2VCExecutionException(
1345 message
="Cannot execute action on charm", primitive_name
=action_name
1348 async def _juju_configure_application(
1351 application_name
: str,
1354 progress_timeout
: float = None,
1355 total_timeout
: float = None,
1358 # get the application
1359 application
= await self
._juju
_get
_application
(
1360 model_name
=model_name
, application_name
=application_name
1364 "configuring the application {} -> {}".format(application_name
, config
)
1366 res
= await application
.set_config(config
)
1368 "application {} configured. res={}".format(application_name
, res
)
1371 # Verify the config is set
1372 new_conf
= await application
.get_config()
1374 value
= new_conf
[key
]["value"]
1375 self
.log
.debug(" {} = {}".format(key
, value
))
1376 if config
[key
] != value
:
1377 raise N2VCException(
1378 message
="key {} is not configured correctly {} != {}".format(
1379 key
, config
[key
], new_conf
[key
]
1383 # check if 'verify-ssh-credentials' action exists
1384 # unit = application.units[0]
1385 actions
= await application
.get_actions()
1386 if "verify-ssh-credentials" not in actions
:
1388 "Action verify-ssh-credentials does not exist in application {}"
1389 ).format(application_name
)
1390 self
.log
.debug(msg
=msg
)
1393 # execute verify-credentials
1395 retry_timeout
= 15.0
1396 for _
in range(num_retries
):
1398 self
.log
.debug("Executing action verify-ssh-credentials...")
1399 output
, ok
= await self
._juju
_execute
_action
(
1400 model_name
=model_name
,
1401 application_name
=application_name
,
1402 action_name
="verify-ssh-credentials",
1404 progress_timeout
=progress_timeout
,
1405 total_timeout
=total_timeout
,
1407 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1409 except asyncio
.CancelledError
:
1411 except Exception as e
:
1413 "Error executing verify-ssh-credentials: {}. Retrying...".format(e
)
1415 await asyncio
.sleep(retry_timeout
)
1418 "Error executing verify-ssh-credentials after {} retries. ".format(
1424 async def _juju_get_application(self
, model_name
: str, application_name
: str):
1425 """Get the deployed application."""
1427 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1429 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1431 if model
.applications
and application_name
in model
.applications
:
1432 return model
.applications
[application_name
]
1434 raise N2VCException(
1435 message
="Cannot get application {} from model {}".format(
1436 application_name
, model_name
1440 async def _juju_get_model(self
, model_name
: str) -> Model
:
1441 """ Get a model object from juju controller
1442 If the model does not exits, it creates it.
1444 :param str model_name: name of the model
1445 :returns Model: model obtained from juju controller or Exception
1449 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1451 if model_name
in self
.juju_models
:
1452 return self
.juju_models
[model_name
]
1454 if self
._creating
_model
:
1455 self
.log
.debug("Another coroutine is creating a model. Wait...")
1456 while self
._creating
_model
:
1457 # another coroutine is creating a model, wait
1458 await asyncio
.sleep(0.1)
1459 # retry (perhaps another coroutine has created the model meanwhile)
1460 if model_name
in self
.juju_models
:
1461 return self
.juju_models
[model_name
]
1464 self
._creating
_model
= True
1466 # get juju model names from juju
1467 model_list
= await self
.controller
.list_models()
1468 if model_name
not in model_list
:
1470 "Model {} does not exist. Creating new model...".format(model_name
)
1472 config_dict
= {"authorized-keys": self
.public_key
}
1474 config_dict
["apt-mirror"] = self
.apt_mirror
1475 if not self
.enable_os_upgrade
:
1476 config_dict
["enable-os-refresh-update"] = False
1477 config_dict
["enable-os-upgrade"] = False
1478 if self
.cloud
in self
.BUILT_IN_CLOUDS
:
1479 model
= await self
.controller
.add_model(
1480 model_name
=model_name
,
1482 cloud_name
=self
.cloud
,
1485 model
= await self
.controller
.add_model(
1486 model_name
=model_name
,
1488 cloud_name
=self
.cloud
,
1489 credential_name
=self
.cloud
,
1491 self
.log
.info("New model created, name={}".format(model_name
))
1494 "Model already exists in juju. Getting model {}".format(model_name
)
1496 model
= await self
.controller
.get_model(model_name
)
1497 self
.log
.debug("Existing model in juju, name={}".format(model_name
))
1499 self
.juju_models
[model_name
] = model
1500 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1503 except Exception as e
:
1504 msg
= "Cannot get model {}. Exception: {}".format(model_name
, e
)
1506 raise N2VCException(msg
)
1508 self
._creating
_model
= False
1510 async def _juju_add_relation(
1513 application_name_1
: str,
1514 application_name_2
: str,
1519 # get juju model and observer
1520 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1522 r1
= "{}:{}".format(application_name_1
, relation_1
)
1523 r2
= "{}:{}".format(application_name_2
, relation_2
)
1525 self
.log
.debug("adding relation: {} -> {}".format(r1
, r2
))
1527 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1528 except JujuAPIError
as e
:
1529 # If one of the applications in the relationship doesn't exist, or the
1530 # relation has already been added,
1531 # let the operation fail silently.
1532 if "not found" in e
.message
:
1534 if "already exists" in e
.message
:
1536 # another execption, raise it
1539 async def _juju_destroy_application(self
, model_name
: str, application_name
: str):
1542 "Destroying application {} in model {}".format(application_name
, model_name
)
1545 # get juju model and observer
1546 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1547 observer
= self
.juju_observers
[model_name
]
1549 application
= model
.applications
.get(application_name
)
1551 observer
.unregister_application(application_name
)
1552 await application
.destroy()
1554 self
.log
.debug("Application not found: {}".format(application_name
))
1556 async def _juju_destroy_machine(
1557 self
, model_name
: str, machine_id
: str, total_timeout
: float = None
1561 "Destroying machine {} in model {}".format(machine_id
, model_name
)
1564 if total_timeout
is None:
1565 total_timeout
= 3600
1567 # get juju model and observer
1568 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1569 observer
= self
.juju_observers
[model_name
]
1571 machines
= await model
.get_machines()
1572 if machine_id
in machines
:
1573 machine
= model
.machines
[machine_id
]
1574 observer
.unregister_machine(machine_id
)
1575 # TODO: change this by machine.is_manual when this is upstreamed:
1576 # https://github.com/juju/python-libjuju/pull/396
1577 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1579 ].startswith("manual:"):
1580 self
.log
.debug("machine.destroy(force=True) started.")
1581 await machine
.destroy(force
=True)
1582 self
.log
.debug("machine.destroy(force=True) passed.")
1584 end
= time
.time() + total_timeout
1585 # wait for machine removal
1586 machines
= await model
.get_machines()
1587 while machine_id
in machines
and time
.time() < end
:
1589 "Waiting for machine {} is destroyed".format(machine_id
)
1591 await asyncio
.sleep(0.5)
1592 machines
= await model
.get_machines()
1593 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1595 self
.log
.debug("Machine not found: {}".format(machine_id
))
1597 async def _juju_destroy_model(self
, model_name
: str, total_timeout
: float = None):
1599 self
.log
.debug("Destroying model {}".format(model_name
))
1601 if total_timeout
is None:
1602 total_timeout
= 3600
1603 end
= time
.time() + total_timeout
1605 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1608 raise N2VCNotFound(message
="Model {} does not exist".format(model_name
))
1610 uuid
= model
.info
.uuid
1612 # destroy applications
1613 for application_name
in model
.applications
:
1615 await self
._juju
_destroy
_application
(
1616 model_name
=model_name
, application_name
=application_name
1618 except Exception as e
:
1620 "Error destroying application {} in model {}: {}".format(
1621 application_name
, model_name
, e
1626 machines
= await model
.get_machines()
1627 for machine_id
in machines
:
1629 await self
._juju
_destroy
_machine
(
1630 model_name
=model_name
, machine_id
=machine_id
1632 except asyncio
.CancelledError
:
1635 # ignore exceptions destroying machine
1638 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1640 self
.log
.debug("destroying model {}...".format(model_name
))
1641 await self
.controller
.destroy_model(uuid
)
1642 # self.log.debug('model destroy requested {}'.format(model_name))
1644 # wait for model is completely destroyed
1645 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
1647 while time
.time() < end
:
1649 # await self.controller.get_model(uuid)
1650 models
= await self
.controller
.list_models()
1651 if model_name
not in models
:
1653 "The model {} ({}) was destroyed".format(model_name
, uuid
)
1656 except asyncio
.CancelledError
:
1658 except Exception as e
:
1660 await asyncio
.sleep(5)
1661 raise N2VCException(
1662 "Timeout waiting for model {} to be destroyed {}".format(
1663 model_name
, last_exception
1667 async def _juju_login(self
):
1668 """Connect to juju controller
1672 # if already authenticated, exit function
1673 if self
._authenticated
:
1676 # if connecting, wait for finish
1677 # another task could be trying to connect in parallel
1678 while self
._connecting
:
1679 await asyncio
.sleep(0.1)
1681 # double check after other task has finished
1682 if self
._authenticated
:
1686 self
._connecting
= True
1688 "connecting to juju controller: {} {}:{}{}".format(
1691 self
.secret
[:8] + "...",
1692 " with ca_cert" if self
.ca_cert
else "",
1696 # Create controller object
1697 self
.controller
= Controller(loop
=self
.loop
)
1698 # Connect to controller
1699 await self
.controller
.connect(
1701 username
=self
.username
,
1702 password
=self
.secret
,
1703 cacert
=self
.ca_cert
,
1705 self
._authenticated
= True
1706 self
.log
.info("juju controller connected")
1707 except Exception as e
:
1708 message
= "Exception connecting to juju: {}".format(e
)
1709 self
.log
.error(message
)
1710 raise N2VCConnectionException(message
=message
, url
=self
.url
)
1712 self
._connecting
= False
1714 async def _juju_logout(self
):
1715 """Logout of the Juju controller."""
1716 if not self
._authenticated
:
1719 # disconnect all models
1720 for model_name
in self
.juju_models
:
1722 await self
._juju
_disconnect
_model
(model_name
)
1723 except Exception as e
:
1725 "Error disconnecting model {} : {}".format(model_name
, e
)
1727 # continue with next model...
1729 self
.log
.info("Disconnecting controller")
1731 await self
.controller
.disconnect()
1732 except Exception as e
:
1733 raise N2VCConnectionException(
1734 message
="Error disconnecting controller: {}".format(e
), url
=self
.url
1737 self
.controller
= None
1738 self
._authenticated
= False
1739 self
.log
.info("disconnected")
1741 async def _juju_disconnect_model(self
, model_name
: str):
1742 self
.log
.debug("Disconnecting model {}".format(model_name
))
1743 if model_name
in self
.juju_models
:
1744 await self
.juju_models
[model_name
].disconnect()
1745 self
.juju_models
[model_name
] = None
1746 self
.juju_observers
[model_name
] = None
1748 self
.warning("Cannot disconnect model: {}".format(model_name
))
1750 def _create_juju_public_key(self
):
1751 """Recreate the Juju public key on lcm container, if needed
1752 Certain libjuju commands expect to be run from the same machine as Juju
1753 is bootstrapped to. This method will write the public key to disk in
1754 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1757 # Make sure that we have a public key before writing to disk
1758 if self
.public_key
is None or len(self
.public_key
) == 0:
1759 if "OSMLCM_VCA_PUBKEY" in os
.environ
:
1760 self
.public_key
= os
.getenv("OSMLCM_VCA_PUBKEY", "")
1761 if len(self
.public_key
) == 0:
1766 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser("~"))
1767 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1769 "writing juju public key to file:\n{}\npublic key: {}".format(
1770 file_path
, self
.public_key
1773 if not os
.path
.exists(pk_path
):
1774 # create path and write file
1775 os
.makedirs(pk_path
)
1776 with
open(file_path
, "w") as f
:
1777 self
.log
.debug("Creating juju public key file: {}".format(file_path
))
1778 f
.write(self
.public_key
)
1780 self
.log
.debug("juju public key file already exists: {}".format(file_path
))
1783 def _format_model_name(name
: str) -> str:
1784 """Format the name of the model.
1786 Model names may only contain lowercase letters, digits and hyphens
1789 return name
.replace("_", "-").replace(" ", "-").lower()
1792 def _format_app_name(name
: str) -> str:
1793 """Format the name of the application (in order to assure valid application name).
1795 Application names have restrictions (run juju deploy --help):
1796 - contains lowercase letters 'a'-'z'
1797 - contains numbers '0'-'9'
1798 - contains hyphens '-'
1799 - starts with a lowercase letter
1800 - not two or more consecutive hyphens
1801 - after a hyphen, not a group with all numbers
1804 def all_numbers(s
: str) -> bool:
1810 new_name
= name
.replace("_", "-")
1811 new_name
= new_name
.replace(" ", "-")
1812 new_name
= new_name
.lower()
1813 while new_name
.find("--") >= 0:
1814 new_name
= new_name
.replace("--", "-")
1815 groups
= new_name
.split("-")
1817 # find 'all numbers' groups and prefix them with a letter
1819 for i
in range(len(groups
)):
1821 if all_numbers(group
):
1827 if app_name
[0].isdigit():
1828 app_name
= "z" + app_name