Check if cloud is built-in cloud when adding a model
[osm/N2VC.git] / n2vc / n2vc_juju_conn.py
1 ##
2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
4 # All Rights Reserved.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15 # implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
21 ##
22
23 import logging
24 import os
25 import asyncio
26 import time
27 import base64
28 import binascii
29 import re
30
31 from n2vc.n2vc_conn import N2VCConnector
32 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
33 from n2vc.exceptions \
34 import N2VCBadArgumentsException, N2VCException, N2VCConnectionException, \
35 N2VCExecutionException, N2VCInvalidCertificate, N2VCNotFound
36 from n2vc.juju_observer import JujuModelObserver
37
38 from juju.controller import Controller
39 from juju.model import Model
40 from juju.application import Application
41 from juju.action import Action
42 from juju.machine import Machine
43 from juju.client import client
44 from juju.errors import JujuAPIError
45
46 from n2vc.provisioner import SSHProvisioner
47
48
49 class N2VCJujuConnector(N2VCConnector):
50
51 """
52 ##################################################################################################
53 ########################################## P U B L I C ###########################################
54 ##################################################################################################
55 """
56
57 BUILT_IN_CLOUDS = ["localhost", "microk8s"]
58
59 def __init__(
60 self,
61 db: object,
62 fs: object,
63 log: object = None,
64 loop: object = None,
65 url: str = '127.0.0.1:17070',
66 username: str = 'admin',
67 vca_config: dict = None,
68 on_update_db=None
69 ):
70 """Initialize juju N2VC connector
71 """
72
73 # parent class constructor
74 N2VCConnector.__init__(
75 self,
76 db=db,
77 fs=fs,
78 log=log,
79 loop=loop,
80 url=url,
81 username=username,
82 vca_config=vca_config,
83 on_update_db=on_update_db
84 )
85
86 # silence websocket traffic log
87 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
88 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
89 logging.getLogger('model').setLevel(logging.WARN)
90
91 self.log.info('Initializing N2VC juju connector...')
92
93 """
94 ##############################################################
95 # check arguments
96 ##############################################################
97 """
98
99 # juju URL
100 if url is None:
101 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
102 url_parts = url.split(':')
103 if len(url_parts) != 2:
104 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url), ['url'])
105 self.hostname = url_parts[0]
106 try:
107 self.port = int(url_parts[1])
108 except ValueError:
109 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url), ['url'])
110
111 # juju USERNAME
112 if username is None:
113 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
114
115 # juju CONFIGURATION
116 if vca_config is None:
117 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
118
119 if 'secret' in vca_config:
120 self.secret = vca_config['secret']
121 else:
122 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
123
124 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
125 # if exists, it will be written in lcm container: _create_juju_public_key()
126 if 'public_key' in vca_config:
127 self.public_key = vca_config['public_key']
128 else:
129 self.public_key = None
130
131 # TODO: Verify ca_cert is valid before using. VCA will crash
132 # if the ca_cert isn't formatted correctly.
133 def base64_to_cacert(b64string):
134 """Convert the base64-encoded string containing the VCA CACERT.
135
136 The input string....
137
138 """
139 try:
140 cacert = base64.b64decode(b64string).decode("utf-8")
141
142 cacert = re.sub(
143 r'\\n',
144 r'\n',
145 cacert,
146 )
147 except binascii.Error as e:
148 self.log.debug("Caught binascii.Error: {}".format(e))
149 raise N2VCInvalidCertificate(message="Invalid CA Certificate")
150
151 return cacert
152
153 self.ca_cert = vca_config.get('ca_cert')
154 if self.ca_cert:
155 self.ca_cert = base64_to_cacert(vca_config['ca_cert'])
156
157 if 'api_proxy' in vca_config:
158 self.api_proxy = vca_config['api_proxy']
159 self.log.debug('api_proxy for native charms configured: {}'.format(self.api_proxy))
160 else:
161 self.warning('api_proxy is not configured. Support for native charms is disabled')
162
163 if 'enable_os_upgrade' in vca_config:
164 self.enable_os_upgrade = vca_config['enable_os_upgrade']
165 else:
166 self.enable_os_upgrade = True
167
168 if 'apt_mirror' in vca_config:
169 self.apt_mirror = vca_config['apt_mirror']
170 else:
171 self.apt_mirror = None
172
173 self.cloud = vca_config.get('cloud')
174 # self.log.debug('Arguments have been checked')
175
176 # juju data
177 self.controller = None # it will be filled when connect to juju
178 self.juju_models = {} # model objects for every model_name
179 self.juju_observers = {} # model observers for every model_name
180 self._connecting = False # while connecting to juju (to avoid duplicate connections)
181 self._authenticated = False # it will be True when juju connection be stablished
182 self._creating_model = False # True during model creation
183
184 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
185 self._create_juju_public_key()
186
187 self.log.info('N2VC juju connector initialized')
188
189 async def get_status(self, namespace: str, yaml_format: bool = True):
190
191 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
192
193 if not self._authenticated:
194 await self._juju_login()
195
196 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
197 # model name is ns_id
198 model_name = ns_id
199 if model_name is None:
200 msg = 'Namespace {} not valid'.format(namespace)
201 self.log.error(msg)
202 raise N2VCBadArgumentsException(msg, ['namespace'])
203
204 # get juju model (create model if needed)
205 model = await self._juju_get_model(model_name=model_name)
206
207 status = await model.get_status()
208
209 if yaml_format:
210 return obj_to_yaml(status)
211 else:
212 return obj_to_dict(status)
213
214 async def create_execution_environment(
215 self,
216 namespace: str,
217 db_dict: dict,
218 reuse_ee_id: str = None,
219 progress_timeout: float = None,
220 total_timeout: float = None
221 ) -> (str, dict):
222
223 self.log.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace, reuse_ee_id))
224
225 if not self._authenticated:
226 await self._juju_login()
227
228 machine_id = None
229 if reuse_ee_id:
230 model_name, application_name, machine_id = self._get_ee_id_components(ee_id=reuse_ee_id)
231 else:
232 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
233 # model name is ns_id
234 model_name = ns_id
235 # application name
236 application_name = self._get_application_name(namespace=namespace)
237
238 self.log.debug('model name: {}, application name: {}, machine_id: {}'
239 .format(model_name, application_name, machine_id))
240
241 # create or reuse a new juju machine
242 try:
243 machine = await self._juju_create_machine(
244 model_name=model_name,
245 application_name=application_name,
246 machine_id=machine_id,
247 db_dict=db_dict,
248 progress_timeout=progress_timeout,
249 total_timeout=total_timeout
250 )
251 except Exception as e:
252 message = 'Error creating machine on juju: {}'.format(e)
253 self.log.error(message)
254 raise N2VCException(message=message)
255
256 # id for the execution environment
257 ee_id = N2VCJujuConnector._build_ee_id(
258 model_name=model_name,
259 application_name=application_name,
260 machine_id=str(machine.entity_id)
261 )
262 self.log.debug('ee_id: {}'.format(ee_id))
263
264 # new machine credentials
265 credentials = dict()
266 credentials['hostname'] = machine.dns_name
267
268 self.log.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id, credentials))
269
270 return ee_id, credentials
271
272 async def register_execution_environment(
273 self,
274 namespace: str,
275 credentials: dict,
276 db_dict: dict,
277 progress_timeout: float = None,
278 total_timeout: float = None
279 ) -> str:
280
281 if not self._authenticated:
282 await self._juju_login()
283
284 self.log.info('Registering execution environment. namespace={}, credentials={}'.format(namespace, credentials))
285
286 if credentials is None:
287 raise N2VCBadArgumentsException(message='credentials are mandatory', bad_args=['credentials'])
288 if credentials.get('hostname'):
289 hostname = credentials['hostname']
290 else:
291 raise N2VCBadArgumentsException(message='hostname is mandatory', bad_args=['credentials.hostname'])
292 if credentials.get('username'):
293 username = credentials['username']
294 else:
295 raise N2VCBadArgumentsException(message='username is mandatory', bad_args=['credentials.username'])
296 if 'private_key_path' in credentials:
297 private_key_path = credentials['private_key_path']
298 else:
299 # if not passed as argument, use generated private key path
300 private_key_path = self.private_key_path
301
302 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
303
304 # model name
305 model_name = ns_id
306 # application name
307 application_name = self._get_application_name(namespace=namespace)
308
309 # register machine on juju
310 try:
311 machine_id = await self._juju_provision_machine(
312 model_name=model_name,
313 hostname=hostname,
314 username=username,
315 private_key_path=private_key_path,
316 db_dict=db_dict,
317 progress_timeout=progress_timeout,
318 total_timeout=total_timeout
319 )
320 except Exception as e:
321 self.log.error('Error registering machine: {}'.format(e))
322 raise N2VCException(message='Error registering machine on juju: {}'.format(e))
323
324 self.log.info('Machine registered: {}'.format(machine_id))
325
326 # id for the execution environment
327 ee_id = N2VCJujuConnector._build_ee_id(
328 model_name=model_name,
329 application_name=application_name,
330 machine_id=str(machine_id)
331 )
332
333 self.log.info('Execution environment registered. ee_id: {}'.format(ee_id))
334
335 return ee_id
336
337 async def install_configuration_sw(
338 self,
339 ee_id: str,
340 artifact_path: str,
341 db_dict: dict,
342 progress_timeout: float = None,
343 total_timeout: float = None,
344 config: dict = None,
345 ):
346
347 self.log.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
348 .format(ee_id, artifact_path, db_dict))
349
350 if not self._authenticated:
351 await self._juju_login()
352
353 # check arguments
354 if ee_id is None or len(ee_id) == 0:
355 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
356 if artifact_path is None or len(artifact_path) == 0:
357 raise N2VCBadArgumentsException(message='artifact_path is mandatory', bad_args=['artifact_path'])
358 if db_dict is None:
359 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
360
361 try:
362 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
363 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
364 except Exception as e:
365 raise N2VCBadArgumentsException(
366 message='ee_id={} is not a valid execution environment id'.format(ee_id),
367 bad_args=['ee_id']
368 )
369
370 # remove // in charm path
371 while artifact_path.find('//') >= 0:
372 artifact_path = artifact_path.replace('//', '/')
373
374 # check charm path
375 if not self.fs.file_exists(artifact_path, mode="dir"):
376 msg = 'artifact path does not exist: {}'.format(artifact_path)
377 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
378
379 if artifact_path.startswith('/'):
380 full_path = self.fs.path + artifact_path
381 else:
382 full_path = self.fs.path + '/' + artifact_path
383
384 try:
385 application, retries = await self._juju_deploy_charm(
386 model_name=model_name,
387 application_name=application_name,
388 charm_path=full_path,
389 machine_id=machine_id,
390 db_dict=db_dict,
391 progress_timeout=progress_timeout,
392 total_timeout=total_timeout,
393 config=config
394 )
395 except Exception as e:
396 raise N2VCException(message='Error desploying charm into ee={} : {}'.format(ee_id, e))
397
398 self.log.info('Configuration sw installed')
399
400 async def get_ee_ssh_public__key(
401 self,
402 ee_id: str,
403 db_dict: dict,
404 progress_timeout: float = None,
405 total_timeout: float = None
406 ) -> str:
407
408 self.log.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id, db_dict))
409
410 if not self._authenticated:
411 await self._juju_login()
412
413 # check arguments
414 if ee_id is None or len(ee_id) == 0:
415 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
416 if db_dict is None:
417 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
418
419 try:
420 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
421 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
422 except Exception as e:
423 raise N2VCBadArgumentsException(
424 message='ee_id={} is not a valid execution environment id'.format(ee_id),
425 bad_args=['ee_id']
426 )
427
428 # try to execute ssh layer primitives (if exist):
429 # generate-ssh-key
430 # get-ssh-public-key
431
432 output = None
433
434 # execute action: generate-ssh-key
435 try:
436 output, status = await self._juju_execute_action(
437 model_name=model_name,
438 application_name=application_name,
439 action_name='generate-ssh-key',
440 db_dict=db_dict,
441 progress_timeout=progress_timeout,
442 total_timeout=total_timeout
443 )
444 except Exception as e:
445 self.log.info('Skipping exception while executing action generate-ssh-key: {}'.format(e))
446
447 # execute action: get-ssh-public-key
448 try:
449 output, status = await self._juju_execute_action(
450 model_name=model_name,
451 application_name=application_name,
452 action_name='get-ssh-public-key',
453 db_dict=db_dict,
454 progress_timeout=progress_timeout,
455 total_timeout=total_timeout
456 )
457 except Exception as e:
458 msg = 'Cannot execute action get-ssh-public-key: {}\n'.format(e)
459 self.log.info(msg)
460 raise N2VCException(msg)
461
462 # return public key if exists
463 return output["pubkey"] if "pubkey" in output else output
464
465 async def add_relation(
466 self,
467 ee_id_1: str,
468 ee_id_2: str,
469 endpoint_1: str,
470 endpoint_2: str
471 ):
472
473 self.log.debug('adding new relation between {} and {}, endpoints: {}, {}'
474 .format(ee_id_1, ee_id_2, endpoint_1, endpoint_2))
475
476 # check arguments
477 if not ee_id_1:
478 message = 'EE 1 is mandatory'
479 self.log.error(message)
480 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1'])
481 if not ee_id_2:
482 message = 'EE 2 is mandatory'
483 self.log.error(message)
484 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_2'])
485 if not endpoint_1:
486 message = 'endpoint 1 is mandatory'
487 self.log.error(message)
488 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_1'])
489 if not endpoint_2:
490 message = 'endpoint 2 is mandatory'
491 self.log.error(message)
492 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_2'])
493
494 if not self._authenticated:
495 await self._juju_login()
496
497 # get the model, the applications and the machines from the ee_id's
498 model_1, app_1, machine_1 = self._get_ee_id_components(ee_id_1)
499 model_2, app_2, machine_2 = self._get_ee_id_components(ee_id_2)
500
501 # model must be the same
502 if model_1 != model_2:
503 message = 'EE models are not the same: {} vs {}'.format(ee_id_1, ee_id_2)
504 self.log.error(message)
505 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1', 'ee_id_2'])
506
507 # add juju relations between two applications
508 try:
509 await self._juju_add_relation(
510 model_name=model_1,
511 application_name_1=app_1,
512 application_name_2=app_2,
513 relation_1=endpoint_1,
514 relation_2=endpoint_2
515 )
516 except Exception as e:
517 message = 'Error adding relation between {} and {}: {}'.format(ee_id_1, ee_id_2, e)
518 self.log.error(message)
519 raise N2VCException(message=message)
520
521 async def remove_relation(
522 self
523 ):
524 if not self._authenticated:
525 await self._juju_login()
526 # TODO
527 self.log.info('Method not implemented yet')
528 raise NotImplemented()
529
530 async def deregister_execution_environments(
531 self
532 ):
533 if not self._authenticated:
534 await self._juju_login()
535 # TODO
536 self.log.info('Method not implemented yet')
537 raise NotImplemented()
538
539 async def delete_namespace(
540 self,
541 namespace: str,
542 db_dict: dict = None,
543 total_timeout: float = None
544 ):
545 self.log.info('Deleting namespace={}'.format(namespace))
546
547 if not self._authenticated:
548 await self._juju_login()
549
550 # check arguments
551 if namespace is None:
552 raise N2VCBadArgumentsException(message='namespace is mandatory', bad_args=['namespace'])
553
554 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
555 if ns_id is not None:
556 try:
557 await self._juju_destroy_model(
558 model_name=ns_id,
559 total_timeout=total_timeout
560 )
561 except N2VCNotFound:
562 raise
563 except Exception as e:
564 raise N2VCException(message='Error deleting namespace {} : {}'.format(namespace, e))
565 else:
566 raise N2VCBadArgumentsException(message='only ns_id is permitted to delete yet', bad_args=['namespace'])
567
568 self.log.info('Namespace {} deleted'.format(namespace))
569
570 async def delete_execution_environment(
571 self,
572 ee_id: str,
573 db_dict: dict = None,
574 total_timeout: float = None
575 ):
576 self.log.info('Deleting execution environment ee_id={}'.format(ee_id))
577
578 if not self._authenticated:
579 await self._juju_login()
580
581 # check arguments
582 if ee_id is None:
583 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
584
585 model_name, application_name, machine_id = self._get_ee_id_components(ee_id=ee_id)
586
587 # destroy the application
588 try:
589 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
590 except Exception as e:
591 raise N2VCException(message='Error deleting execution environment {} (application {}) : {}'
592 .format(ee_id, application_name, e))
593
594 # destroy the machine
595 # try:
596 # await self._juju_destroy_machine(
597 # model_name=model_name,
598 # machine_id=machine_id,
599 # total_timeout=total_timeout
600 # )
601 # except Exception as e:
602 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
603 # .format(ee_id, machine_id, e))
604
605 self.log.info('Execution environment {} deleted'.format(ee_id))
606
607 async def exec_primitive(
608 self,
609 ee_id: str,
610 primitive_name: str,
611 params_dict: dict,
612 db_dict: dict = None,
613 progress_timeout: float = None,
614 total_timeout: float = None
615 ) -> str:
616
617 self.log.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name, ee_id, params_dict))
618
619 if not self._authenticated:
620 await self._juju_login()
621
622 # check arguments
623 if ee_id is None or len(ee_id) == 0:
624 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
625 if primitive_name is None or len(primitive_name) == 0:
626 raise N2VCBadArgumentsException(message='action_name is mandatory', bad_args=['action_name'])
627 if params_dict is None:
628 params_dict = dict()
629
630 try:
631 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
632 except Exception:
633 raise N2VCBadArgumentsException(
634 message='ee_id={} is not a valid execution environment id'.format(ee_id),
635 bad_args=['ee_id']
636 )
637
638 if primitive_name == 'config':
639 # Special case: config primitive
640 try:
641 await self._juju_configure_application(
642 model_name=model_name,
643 application_name=application_name,
644 config=params_dict,
645 db_dict=db_dict,
646 progress_timeout=progress_timeout,
647 total_timeout=total_timeout
648 )
649 except Exception as e:
650 self.log.error('Error configuring juju application: {}'.format(e))
651 raise N2VCExecutionException(
652 message='Error configuring application into ee={} : {}'.format(ee_id, e),
653 primitive_name=primitive_name
654 )
655 return 'CONFIG OK'
656 else:
657 try:
658 output, status = await self._juju_execute_action(
659 model_name=model_name,
660 application_name=application_name,
661 action_name=primitive_name,
662 db_dict=db_dict,
663 progress_timeout=progress_timeout,
664 total_timeout=total_timeout,
665 **params_dict
666 )
667 if status == 'completed':
668 return output
669 else:
670 raise Exception('status is not completed: {}'.format(status))
671 except Exception as e:
672 self.log.error('Error executing primitive {}: {}'.format(primitive_name, e))
673 raise N2VCExecutionException(
674 message='Error executing primitive {} into ee={} : {}'.format(primitive_name, ee_id, e),
675 primitive_name=primitive_name
676 )
677
678 async def disconnect(self):
679 self.log.info('closing juju N2VC...')
680 await self._juju_logout()
681
682 """
683 ##################################################################################################
684 ########################################## P R I V A T E #########################################
685 ##################################################################################################
686 """
687
688 def _write_ee_id_db(
689 self,
690 db_dict: dict,
691 ee_id: str
692 ):
693
694 # write ee_id to database: _admin.deployed.VCA.x
695 try:
696 the_table = db_dict['collection']
697 the_filter = db_dict['filter']
698 the_path = db_dict['path']
699 if not the_path[-1] == '.':
700 the_path = the_path + '.'
701 update_dict = {the_path + 'ee_id': ee_id}
702 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
703 self.db.set_one(
704 table=the_table,
705 q_filter=the_filter,
706 update_dict=update_dict,
707 fail_on_empty=True
708 )
709 except asyncio.CancelledError:
710 raise
711 except Exception as e:
712 self.log.error('Error writing ee_id to database: {}'.format(e))
713
714 @staticmethod
715 def _build_ee_id(
716 model_name: str,
717 application_name: str,
718 machine_id: str
719 ):
720 """
721 Build an execution environment id form model, application and machine
722 :param model_name:
723 :param application_name:
724 :param machine_id:
725 :return:
726 """
727 # id for the execution environment
728 return '{}.{}.{}'.format(model_name, application_name, machine_id)
729
730 @staticmethod
731 def _get_ee_id_components(
732 ee_id: str
733 ) -> (str, str, str):
734 """
735 Get model, application and machine components from an execution environment id
736 :param ee_id:
737 :return: model_name, application_name, machine_id
738 """
739
740 if ee_id is None:
741 return None, None, None
742
743 # split components of id
744 parts = ee_id.split('.')
745 model_name = parts[0]
746 application_name = parts[1]
747 machine_id = parts[2]
748 return model_name, application_name, machine_id
749
750 def _get_application_name(self, namespace: str) -> str:
751 """
752 Build application name from namespace
753 :param namespace:
754 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
755 """
756
757 # TODO: Enforce the Juju 50-character application limit
758
759 # split namespace components
760 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
761
762 if vnf_id is None or len(vnf_id) == 0:
763 vnf_id = ''
764 else:
765 # Shorten the vnf_id to its last twelve characters
766 vnf_id = 'vnf-' + vnf_id[-12:]
767
768 if vdu_id is None or len(vdu_id) == 0:
769 vdu_id = ''
770 else:
771 # Shorten the vdu_id to its last twelve characters
772 vdu_id = '-vdu-' + vdu_id[-12:]
773
774 if vdu_count is None or len(vdu_count) == 0:
775 vdu_count = ''
776 else:
777 vdu_count = '-cnt-' + vdu_count
778
779 application_name = 'app-{}{}{}'.format(vnf_id, vdu_id, vdu_count)
780
781 return N2VCJujuConnector._format_app_name(application_name)
782
783 async def _juju_create_machine(
784 self,
785 model_name: str,
786 application_name: str,
787 machine_id: str = None,
788 db_dict: dict = None,
789 progress_timeout: float = None,
790 total_timeout: float = None
791 ) -> Machine:
792
793 self.log.debug('creating machine in model: {}, existing machine id: {}'.format(model_name, machine_id))
794
795 # get juju model and observer (create model if needed)
796 model = await self._juju_get_model(model_name=model_name)
797 observer = self.juju_observers[model_name]
798
799 # find machine id in model
800 machine = None
801 if machine_id is not None:
802 self.log.debug('Finding existing machine id {} in model'.format(machine_id))
803 # get juju existing machines in the model
804 existing_machines = await model.get_machines()
805 if machine_id in existing_machines:
806 self.log.debug('Machine id {} found in model (reusing it)'.format(machine_id))
807 machine = model.machines[machine_id]
808
809 if machine is None:
810 self.log.debug('Creating a new machine in juju...')
811 # machine does not exist, create it and wait for it
812 machine = await model.add_machine(
813 spec=None,
814 constraints=None,
815 disks=None,
816 series='xenial'
817 )
818
819 # register machine with observer
820 observer.register_machine(machine=machine, db_dict=db_dict)
821
822 # id for the execution environment
823 ee_id = N2VCJujuConnector._build_ee_id(
824 model_name=model_name,
825 application_name=application_name,
826 machine_id=str(machine.entity_id)
827 )
828
829 # write ee_id in database
830 self._write_ee_id_db(
831 db_dict=db_dict,
832 ee_id=ee_id
833 )
834
835 # wait for machine creation
836 await observer.wait_for_machine(
837 machine_id=str(machine.entity_id),
838 progress_timeout=progress_timeout,
839 total_timeout=total_timeout
840 )
841
842 else:
843
844 self.log.debug('Reusing old machine pending')
845
846 # register machine with observer
847 observer.register_machine(machine=machine, db_dict=db_dict)
848
849 # machine does exist, but it is in creation process (pending), wait for create finalisation
850 await observer.wait_for_machine(
851 machine_id=machine.entity_id,
852 progress_timeout=progress_timeout,
853 total_timeout=total_timeout)
854
855 self.log.debug("Machine ready at " + str(machine.dns_name))
856 return machine
857
858 async def _juju_provision_machine(
859 self,
860 model_name: str,
861 hostname: str,
862 username: str,
863 private_key_path: str,
864 db_dict: dict = None,
865 progress_timeout: float = None,
866 total_timeout: float = None
867 ) -> str:
868
869 if not self.api_proxy:
870 msg = 'Cannot provision machine: api_proxy is not defined'
871 self.log.error(msg=msg)
872 raise N2VCException(message=msg)
873
874 self.log.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name, hostname, username))
875
876 if not self._authenticated:
877 await self._juju_login()
878
879 # get juju model and observer
880 model = await self._juju_get_model(model_name=model_name)
881 observer = self.juju_observers[model_name]
882
883 # TODO check if machine is already provisioned
884 machine_list = await model.get_machines()
885
886 provisioner = SSHProvisioner(
887 host=hostname,
888 user=username,
889 private_key_path=private_key_path,
890 log=self.log
891 )
892
893 params = None
894 try:
895 params = provisioner.provision_machine()
896 except Exception as ex:
897 msg = "Exception provisioning machine: {}".format(ex)
898 self.log.error(msg)
899 raise N2VCException(message=msg)
900
901 params.jobs = ['JobHostUnits']
902
903 connection = model.connection()
904
905 # Submit the request.
906 self.log.debug("Adding machine to model")
907 client_facade = client.ClientFacade.from_connection(connection)
908 results = await client_facade.AddMachines(params=[params])
909 error = results.machines[0].error
910 if error:
911 msg = "Error adding machine: {}}".format(error.message)
912 self.log.error(msg=msg)
913 raise ValueError(msg)
914
915 machine_id = results.machines[0].machine
916
917 # Need to run this after AddMachines has been called,
918 # as we need the machine_id
919 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
920 asyncio.ensure_future(provisioner.install_agent(
921 connection=connection,
922 nonce=params.nonce,
923 machine_id=machine_id,
924 api=self.api_proxy,
925 ))
926
927 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
928 machine = None
929 for i in range(10):
930 machine_list = await model.get_machines()
931 if machine_id in machine_list:
932 self.log.debug('Machine {} found in model!'.format(machine_id))
933 machine = model.machines.get(machine_id)
934 break
935 await asyncio.sleep(2)
936
937 if machine is None:
938 msg = 'Machine {} not found in model'.format(machine_id)
939 self.log.error(msg=msg)
940 raise Exception(msg)
941
942 # register machine with observer
943 observer.register_machine(machine=machine, db_dict=db_dict)
944
945 # wait for machine creation
946 self.log.debug('waiting for provision finishes... {}'.format(machine_id))
947 await observer.wait_for_machine(
948 machine_id=machine_id,
949 progress_timeout=progress_timeout,
950 total_timeout=total_timeout
951 )
952
953 self.log.debug("Machine provisioned {}".format(machine_id))
954
955 return machine_id
956
957 async def _juju_deploy_charm(
958 self,
959 model_name: str,
960 application_name: str,
961 charm_path: str,
962 machine_id: str,
963 db_dict: dict,
964 progress_timeout: float = None,
965 total_timeout: float = None,
966 config: dict = None
967 ) -> (Application, int):
968
969 # get juju model and observer
970 model = await self._juju_get_model(model_name=model_name)
971 observer = self.juju_observers[model_name]
972
973 # check if application already exists
974 application = None
975 if application_name in model.applications:
976 application = model.applications[application_name]
977
978 if application is None:
979
980 # application does not exist, create it and wait for it
981 self.log.debug('deploying application {} to machine {}, model {}'
982 .format(application_name, machine_id, model_name))
983 self.log.debug('charm: {}'.format(charm_path))
984 series = 'xenial'
985 # series = None
986 application = await model.deploy(
987 entity_url=charm_path,
988 application_name=application_name,
989 channel='stable',
990 num_units=1,
991 series=series,
992 to=machine_id,
993 config=config
994 )
995
996 # register application with observer
997 observer.register_application(application=application, db_dict=db_dict)
998
999 self.log.debug('waiting for application deployed... {}'.format(application.entity_id))
1000 retries = await observer.wait_for_application(
1001 application_id=application.entity_id,
1002 progress_timeout=progress_timeout,
1003 total_timeout=total_timeout)
1004 self.log.debug('application deployed')
1005
1006 else:
1007
1008 # register application with observer
1009 observer.register_application(application=application, db_dict=db_dict)
1010
1011 # application already exists, but not finalised
1012 self.log.debug('application already exists, waiting for deployed...')
1013 retries = await observer.wait_for_application(
1014 application_id=application.entity_id,
1015 progress_timeout=progress_timeout,
1016 total_timeout=total_timeout)
1017 self.log.debug('application deployed')
1018
1019 return application, retries
1020
1021 async def _juju_execute_action(
1022 self,
1023 model_name: str,
1024 application_name: str,
1025 action_name: str,
1026 db_dict: dict,
1027 progress_timeout: float = None,
1028 total_timeout: float = None,
1029 **kwargs
1030 ) -> Action:
1031
1032 # get juju model and observer
1033 model = await self._juju_get_model(model_name=model_name)
1034 observer = self.juju_observers[model_name]
1035
1036 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1037
1038 unit = None
1039 for u in application.units:
1040 if await u.is_leader_from_status():
1041 unit = u
1042 if unit is not None:
1043 actions = await application.get_actions()
1044 if action_name in actions:
1045 self.log.debug('executing action "{}" using params: {}'.format(action_name, kwargs))
1046 action = await unit.run_action(action_name, **kwargs)
1047
1048 # register action with observer
1049 observer.register_action(action=action, db_dict=db_dict)
1050
1051 await observer.wait_for_action(
1052 action_id=action.entity_id,
1053 progress_timeout=progress_timeout,
1054 total_timeout=total_timeout)
1055 self.log.debug('action completed with status: {}'.format(action.status))
1056 output = await model.get_action_output(action_uuid=action.entity_id)
1057 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1058 if action.entity_id in status:
1059 status = status[action.entity_id]
1060 else:
1061 status = 'failed'
1062 return output, status
1063
1064 raise N2VCExecutionException(
1065 message='Cannot execute action on charm',
1066 primitive_name=action_name
1067 )
1068
1069 async def _juju_configure_application(
1070 self,
1071 model_name: str,
1072 application_name: str,
1073 config: dict,
1074 db_dict: dict,
1075 progress_timeout: float = None,
1076 total_timeout: float = None
1077 ):
1078
1079 # get the application
1080 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1081
1082 self.log.debug('configuring the application {} -> {}'.format(application_name, config))
1083 res = await application.set_config(config)
1084 self.log.debug('application {} configured. res={}'.format(application_name, res))
1085
1086 # Verify the config is set
1087 new_conf = await application.get_config()
1088 for key in config:
1089 value = new_conf[key]['value']
1090 self.log.debug(' {} = {}'.format(key, value))
1091 if config[key] != value:
1092 raise N2VCException(
1093 message='key {} is not configured correctly {} != {}'.format(key, config[key], new_conf[key])
1094 )
1095
1096 # check if 'verify-ssh-credentials' action exists
1097 # unit = application.units[0]
1098 actions = await application.get_actions()
1099 if 'verify-ssh-credentials' not in actions:
1100 msg = 'Action verify-ssh-credentials does not exist in application {}'.format(application_name)
1101 self.log.debug(msg=msg)
1102 return False
1103
1104 # execute verify-credentials
1105 num_retries = 20
1106 retry_timeout = 15.0
1107 for i in range(num_retries):
1108 try:
1109 self.log.debug('Executing action verify-ssh-credentials...')
1110 output, ok = await self._juju_execute_action(
1111 model_name=model_name,
1112 application_name=application_name,
1113 action_name='verify-ssh-credentials',
1114 db_dict=db_dict,
1115 progress_timeout=progress_timeout,
1116 total_timeout=total_timeout
1117 )
1118 self.log.debug('Result: {}, output: {}'.format(ok, output))
1119 return True
1120 except asyncio.CancelledError:
1121 raise
1122 except Exception as e:
1123 self.log.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e))
1124 await asyncio.sleep(retry_timeout)
1125 else:
1126 self.log.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries))
1127 return False
1128
1129 async def _juju_get_application(
1130 self,
1131 model_name: str,
1132 application_name: str
1133 ):
1134 """Get the deployed application."""
1135
1136 model = await self._juju_get_model(model_name=model_name)
1137
1138 application_name = N2VCJujuConnector._format_app_name(application_name)
1139
1140 if model.applications and application_name in model.applications:
1141 return model.applications[application_name]
1142 else:
1143 raise N2VCException(message='Cannot get application {} from model {}'.format(application_name, model_name))
1144
1145 async def _juju_get_model(self, model_name: str) -> Model:
1146 """ Get a model object from juju controller
1147 If the model does not exits, it creates it.
1148
1149 :param str model_name: name of the model
1150 :returns Model: model obtained from juju controller or Exception
1151 """
1152
1153 # format model name
1154 model_name = N2VCJujuConnector._format_model_name(model_name)
1155
1156 if model_name in self.juju_models:
1157 return self.juju_models[model_name]
1158
1159 if self._creating_model:
1160 self.log.debug('Another coroutine is creating a model. Wait...')
1161 while self._creating_model:
1162 # another coroutine is creating a model, wait
1163 await asyncio.sleep(0.1)
1164 # retry (perhaps another coroutine has created the model meanwhile)
1165 if model_name in self.juju_models:
1166 return self.juju_models[model_name]
1167
1168 try:
1169 self._creating_model = True
1170
1171 # get juju model names from juju
1172 model_list = await self.controller.list_models()
1173
1174 if model_name not in model_list:
1175 self.log.info('Model {} does not exist. Creating new model...'.format(model_name))
1176 config_dict = {'authorized-keys': self.public_key}
1177 if self.apt_mirror:
1178 config_dict['apt-mirror'] = self.apt_mirror
1179 if not self.enable_os_upgrade:
1180 config_dict['enable-os-refresh-update'] = False
1181 config_dict['enable-os-upgrade'] = False
1182 if self.cloud in self.BUILT_IN_CLOUDS:
1183 model = await self.controller.add_model(
1184 model_name=model_name,
1185 config=config_dict,
1186 cloud_name=self.cloud,
1187 )
1188 else:
1189 model = await self.controller.add_model(
1190 model_name=model_name,
1191 config=config_dict,
1192 cloud_name=self.cloud,
1193 credential_name="admin"
1194 )
1195 self.log.info('New model created, name={}'.format(model_name))
1196 else:
1197 self.log.debug('Model already exists in juju. Getting model {}'.format(model_name))
1198 model = await self.controller.get_model(model_name)
1199 self.log.debug('Existing model in juju, name={}'.format(model_name))
1200
1201 self.juju_models[model_name] = model
1202 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1203 return model
1204
1205 except Exception as e:
1206 msg = 'Cannot get model {}. Exception: {}'.format(model_name, e)
1207 self.log.error(msg)
1208 raise N2VCException(msg)
1209 finally:
1210 self._creating_model = False
1211
1212 async def _juju_add_relation(
1213 self,
1214 model_name: str,
1215 application_name_1: str,
1216 application_name_2: str,
1217 relation_1: str,
1218 relation_2: str
1219 ):
1220
1221 # get juju model and observer
1222 model = await self._juju_get_model(model_name=model_name)
1223
1224 r1 = '{}:{}'.format(application_name_1, relation_1)
1225 r2 = '{}:{}'.format(application_name_2, relation_2)
1226
1227 self.log.debug('adding relation: {} -> {}'.format(r1, r2))
1228 try:
1229 await model.add_relation(relation1=r1, relation2=r2)
1230 except JujuAPIError as e:
1231 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1232 # let the operation fail silently.
1233 if 'not found' in e.message:
1234 return
1235 if 'already exists' in e.message:
1236 return
1237 # another execption, raise it
1238 raise e
1239
1240 async def _juju_destroy_application(
1241 self,
1242 model_name: str,
1243 application_name: str
1244 ):
1245
1246 self.log.debug('Destroying application {} in model {}'.format(application_name, model_name))
1247
1248 # get juju model and observer
1249 model = await self._juju_get_model(model_name=model_name)
1250 observer = self.juju_observers[model_name]
1251
1252 application = model.applications.get(application_name)
1253 if application:
1254 observer.unregister_application(application_name)
1255 await application.destroy()
1256 else:
1257 self.log.debug('Application not found: {}'.format(application_name))
1258
1259 async def _juju_destroy_machine(
1260 self,
1261 model_name: str,
1262 machine_id: str,
1263 total_timeout: float = None
1264 ):
1265
1266 self.log.debug('Destroying machine {} in model {}'.format(machine_id, model_name))
1267
1268 if total_timeout is None:
1269 total_timeout = 3600
1270
1271 # get juju model and observer
1272 model = await self._juju_get_model(model_name=model_name)
1273 observer = self.juju_observers[model_name]
1274
1275 machines = await model.get_machines()
1276 if machine_id in machines:
1277 machine = model.machines[machine_id]
1278 observer.unregister_machine(machine_id)
1279 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1280 if "instance-id" in machine.safe_data and machine.safe_data[
1281 "instance-id"
1282 ].startswith("manual:"):
1283 self.log.debug("machine.destroy(force=True) started.")
1284 await machine.destroy(force=True)
1285 self.log.debug("machine.destroy(force=True) passed.")
1286 # max timeout
1287 end = time.time() + total_timeout
1288 # wait for machine removal
1289 machines = await model.get_machines()
1290 while machine_id in machines and time.time() < end:
1291 self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
1292 await asyncio.sleep(0.5)
1293 machines = await model.get_machines()
1294 self.log.debug("Machine destroyed: {}".format(machine_id))
1295 else:
1296 self.log.debug('Machine not found: {}'.format(machine_id))
1297
1298 async def _juju_destroy_model(
1299 self,
1300 model_name: str,
1301 total_timeout: float = None
1302 ):
1303
1304 self.log.debug('Destroying model {}'.format(model_name))
1305
1306 if total_timeout is None:
1307 total_timeout = 3600
1308 end = time.time() + total_timeout
1309
1310 model = await self._juju_get_model(model_name=model_name)
1311
1312 if not model:
1313 raise N2VCNotFound(
1314 message="Model {} does not exist".format(model_name)
1315 )
1316
1317 uuid = model.info.uuid
1318
1319 # destroy applications
1320 for application_name in model.applications:
1321 try:
1322 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
1323 except Exception as e:
1324 self.log.error(
1325 "Error destroying application {} in model {}: {}".format(
1326 application_name,
1327 model_name,
1328 e
1329 )
1330 )
1331
1332 # destroy machines
1333 machines = await model.get_machines()
1334 for machine_id in machines:
1335 try:
1336 await self._juju_destroy_machine(model_name=model_name, machine_id=machine_id)
1337 except asyncio.CancelledError:
1338 raise
1339 except Exception as e:
1340 # ignore exceptions destroying machine
1341 pass
1342
1343 await self._juju_disconnect_model(model_name=model_name)
1344
1345 self.log.debug('destroying model {}...'.format(model_name))
1346 await self.controller.destroy_model(uuid)
1347 # self.log.debug('model destroy requested {}'.format(model_name))
1348
1349 # wait for model is completely destroyed
1350 self.log.debug('Waiting for model {} to be destroyed...'.format(model_name))
1351 last_exception = ''
1352 while time.time() < end:
1353 try:
1354 # await self.controller.get_model(uuid)
1355 models = await self.controller.list_models()
1356 if model_name not in models:
1357 self.log.debug('The model {} ({}) was destroyed'.format(model_name, uuid))
1358 return
1359 except asyncio.CancelledError:
1360 raise
1361 except Exception as e:
1362 last_exception = e
1363 await asyncio.sleep(5)
1364 raise N2VCException("Timeout waiting for model {} to be destroyed {}".format(model_name, last_exception))
1365
1366 async def _juju_login(self):
1367 """Connect to juju controller
1368
1369 """
1370
1371 # if already authenticated, exit function
1372 if self._authenticated:
1373 return
1374
1375 # if connecting, wait for finish
1376 # another task could be trying to connect in parallel
1377 while self._connecting:
1378 await asyncio.sleep(0.1)
1379
1380 # double check after other task has finished
1381 if self._authenticated:
1382 return
1383
1384 try:
1385 self._connecting = True
1386 self.log.info(
1387 'connecting to juju controller: {} {}:{}{}'
1388 .format(self.url, self.username, self.secret[:8] + '...', ' with ca_cert' if self.ca_cert else ''))
1389
1390 # Create controller object
1391 self.controller = Controller(loop=self.loop)
1392 # Connect to controller
1393 await self.controller.connect(
1394 endpoint=self.url,
1395 username=self.username,
1396 password=self.secret,
1397 cacert=self.ca_cert
1398 )
1399 self._authenticated = True
1400 self.log.info('juju controller connected')
1401 except Exception as e:
1402 message = 'Exception connecting to juju: {}'.format(e)
1403 self.log.error(message)
1404 raise N2VCConnectionException(
1405 message=message,
1406 url=self.url
1407 )
1408 finally:
1409 self._connecting = False
1410
1411 async def _juju_logout(self):
1412 """Logout of the Juju controller."""
1413 if not self._authenticated:
1414 return False
1415
1416 # disconnect all models
1417 for model_name in self.juju_models:
1418 try:
1419 await self._juju_disconnect_model(model_name)
1420 except Exception as e:
1421 self.log.error('Error disconnecting model {} : {}'.format(model_name, e))
1422 # continue with next model...
1423
1424 self.log.info("Disconnecting controller")
1425 try:
1426 await self.controller.disconnect()
1427 except Exception as e:
1428 raise N2VCConnectionException(message='Error disconnecting controller: {}'.format(e), url=self.url)
1429
1430 self.controller = None
1431 self._authenticated = False
1432 self.log.info('disconnected')
1433
1434 async def _juju_disconnect_model(
1435 self,
1436 model_name: str
1437 ):
1438 self.log.debug("Disconnecting model {}".format(model_name))
1439 if model_name in self.juju_models:
1440 await self.juju_models[model_name].disconnect()
1441 self.juju_models[model_name] = None
1442 self.juju_observers[model_name] = None
1443 else:
1444 self.warning('Cannot disconnect model: {}'.format(model_name))
1445
1446 def _create_juju_public_key(self):
1447 """Recreate the Juju public key on lcm container, if needed
1448 Certain libjuju commands expect to be run from the same machine as Juju
1449 is bootstrapped to. This method will write the public key to disk in
1450 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1451 """
1452
1453 # Make sure that we have a public key before writing to disk
1454 if self.public_key is None or len(self.public_key) == 0:
1455 if 'OSMLCM_VCA_PUBKEY' in os.environ:
1456 self.public_key = os.getenv('OSMLCM_VCA_PUBKEY', '')
1457 if len(self.public_key) == 0:
1458 return
1459 else:
1460 return
1461
1462 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser('~'))
1463 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1464 self.log.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path, self.public_key))
1465 if not os.path.exists(pk_path):
1466 # create path and write file
1467 os.makedirs(pk_path)
1468 with open(file_path, 'w') as f:
1469 self.log.debug('Creating juju public key file: {}'.format(file_path))
1470 f.write(self.public_key)
1471 else:
1472 self.log.debug('juju public key file already exists: {}'.format(file_path))
1473
1474 @staticmethod
1475 def _format_model_name(name: str) -> str:
1476 """Format the name of the model.
1477
1478 Model names may only contain lowercase letters, digits and hyphens
1479 """
1480
1481 return name.replace('_', '-').replace(' ', '-').lower()
1482
1483 @staticmethod
1484 def _format_app_name(name: str) -> str:
1485 """Format the name of the application (in order to assure valid application name).
1486
1487 Application names have restrictions (run juju deploy --help):
1488 - contains lowercase letters 'a'-'z'
1489 - contains numbers '0'-'9'
1490 - contains hyphens '-'
1491 - starts with a lowercase letter
1492 - not two or more consecutive hyphens
1493 - after a hyphen, not a group with all numbers
1494 """
1495
1496 def all_numbers(s: str) -> bool:
1497 for c in s:
1498 if not c.isdigit():
1499 return False
1500 return True
1501
1502 new_name = name.replace('_', '-')
1503 new_name = new_name.replace(' ', '-')
1504 new_name = new_name.lower()
1505 while new_name.find('--') >= 0:
1506 new_name = new_name.replace('--', '-')
1507 groups = new_name.split('-')
1508
1509 # find 'all numbers' groups and prefix them with a letter
1510 app_name = ''
1511 for i in range(len(groups)):
1512 group = groups[i]
1513 if all_numbers(group):
1514 group = 'z' + group
1515 if i > 0:
1516 app_name += '-'
1517 app_name += group
1518
1519 if app_name[0].isdigit():
1520 app_name = 'z' + app_name
1521
1522 return app_name