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 n2vc
.n2vc_conn
import N2VCConnector
32 from n2vc
.exceptions \
33 import N2VCBadArgumentsException
, N2VCException
, N2VCConnectionException
, \
34 N2VCExecutionException
, N2VCInvalidCertificate
35 from n2vc
.juju_observer
import JujuModelObserver
37 from juju
.controller
import Controller
38 from juju
.model
import Model
39 from juju
.application
import Application
40 from juju
.action
import Action
41 from juju
.machine
import Machine
44 class N2VCJujuConnector(N2VCConnector
):
47 ##################################################################################################
48 ########################################## P U B L I C ###########################################
49 ##################################################################################################
58 url
: str = '127.0.0.1:17070',
59 username
: str = 'admin',
60 vca_config
: dict = None,
64 """Initialize juju N2VC connector
67 # parent class constructor
68 N2VCConnector
.__init
__(
76 vca_config
=vca_config
,
77 on_update_db
=on_update_db
80 # silence websocket traffic log
81 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
82 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
83 logging
.getLogger('model').setLevel(logging
.WARN
)
85 self
.info('Initializing N2VC juju connector...')
88 ##############################################################
90 ##############################################################
95 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
96 url_parts
= url
.split(':')
97 if len(url_parts
) != 2:
98 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url
), ['url'])
99 self
.hostname
= url_parts
[0]
101 self
.port
= int(url_parts
[1])
103 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url
), ['url'])
107 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
110 if vca_config
is None:
111 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
113 if 'secret' in vca_config
:
114 self
.secret
= vca_config
['secret']
116 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
118 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
119 # if exists, it will be written in lcm container: _create_juju_public_key()
120 if 'public_key' in vca_config
:
121 self
.public_key
= vca_config
['public_key']
123 self
.public_key
= None
125 # TODO: Verify ca_cert is valid before using. VCA will crash
126 # if the ca_cert isn't formatted correctly.
127 def base64_to_cacert(b64string
):
128 """Convert the base64-encoded string containing the VCA CACERT.
134 cacert
= base64
.b64decode(b64string
).decode("utf-8")
141 except binascii
.Error
as e
:
142 self
.debug("Caught binascii.Error: {}".format(e
))
143 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
147 self
.ca_cert
= vca_config
.get('ca_cert')
149 self
.ca_cert
= base64_to_cacert(vca_config
['ca_cert'])
152 self
.api_proxy
= api_proxy
154 self
.warning('api_proxy is not configured. Support for native charms is disabled')
156 self
.debug('Arguments have been checked')
159 self
.controller
= None # it will be filled when connect to juju
160 self
.juju_models
= {} # model objects for every model_name
161 self
.juju_observers
= {} # model observers for every model_name
162 self
._connecting
= False # while connecting to juju (to avoid duplicate connections)
163 self
._authenticated
= False # it will be True when juju connection be stablished
164 self
._creating
_model
= False # True during model creation
166 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
167 self
._create
_juju
_public
_key
()
169 self
.info('N2VC juju connector initialized')
171 async def get_status(self
, namespace
: str):
172 self
.info('Getting NS status. namespace: {}'.format(namespace
))
174 if not self
._authenticated
:
175 await self
._juju
_login
()
177 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
178 # model name is ns_id
180 if model_name
is None:
181 msg
= 'Namespace {} not valid'.format(namespace
)
183 raise N2VCBadArgumentsException(msg
, ['namespace'])
185 # get juju model (create model if needed)
186 model
= await self
._juju
_get
_model
(model_name
=model_name
)
188 status
= await model
.get_status()
192 async def create_execution_environment(
196 reuse_ee_id
: str = None,
197 progress_timeout
: float = None,
198 total_timeout
: float = None
201 self
.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace
, reuse_ee_id
))
203 if not self
._authenticated
:
204 await self
._juju
_login
()
208 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=reuse_ee_id
)
210 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
211 # model name is ns_id
214 application_name
= self
._get
_application
_name
(namespace
=namespace
)
216 self
.debug('model name: {}, application name: {}, machine_id: {}'
217 .format(model_name
, application_name
, machine_id
))
219 # create or reuse a new juju machine
221 machine
= await self
._juju
_create
_machine
(
222 model_name
=model_name
,
223 application_name
=application_name
,
224 machine_id
=machine_id
,
226 progress_timeout
=progress_timeout
,
227 total_timeout
=total_timeout
229 except Exception as e
:
230 message
= 'Error creating machine on juju: {}'.format(e
)
232 raise N2VCException(message
=message
)
234 # id for the execution environment
235 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
236 model_name
=model_name
,
237 application_name
=application_name
,
238 machine_id
=str(machine
.entity_id
)
240 self
.debug('ee_id: {}'.format(ee_id
))
242 # new machine credentials
244 credentials
['hostname'] = machine
.dns_name
246 self
.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id
, credentials
))
248 return ee_id
, credentials
250 async def register_execution_environment(
255 progress_timeout
: float = None,
256 total_timeout
: float = None
259 if not self
._authenticated
:
260 await self
._juju
_login
()
262 self
.info('Registering execution environment. namespace={}, credentials={}'.format(namespace
, credentials
))
264 if credentials
is None:
265 raise N2VCBadArgumentsException(message
='credentials are mandatory', bad_args
=['credentials'])
266 if 'hostname' in credentials
:
267 hostname
= credentials
['hostname']
269 raise N2VCBadArgumentsException(message
='hostname is mandatory', bad_args
=['credentials.hostname'])
270 if 'username' in credentials
:
271 username
= credentials
['username']
273 raise N2VCBadArgumentsException(message
='username is mandatory', bad_args
=['credentials.username'])
274 if 'private_key_path' in credentials
:
275 private_key_path
= credentials
['private_key_path']
277 # if not passed as argument, use generated private key path
278 private_key_path
= self
.private_key_path
280 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
285 application_name
= self
._get
_application
_name
(namespace
=namespace
)
287 # register machine on juju
289 machine
= await self
._juju
_provision
_machine
(
290 model_name
=model_name
,
293 private_key_path
=private_key_path
,
295 progress_timeout
=progress_timeout
,
296 total_timeout
=total_timeout
298 except Exception as e
:
299 self
.error('Error registering machine: {}'.format(e
))
300 raise N2VCException(message
='Error registering machine on juju: {}'.format(e
))
301 self
.info('Machine registered')
303 # id for the execution environment
304 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
305 model_name
=model_name
,
306 application_name
=application_name
,
307 machine_id
=str(machine
.entity_id
)
310 self
.info('Execution environment registered. ee_id: {}'.format(ee_id
))
314 async def install_configuration_sw(
319 progress_timeout
: float = None,
320 total_timeout
: float = None
323 self
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
324 .format(ee_id
, artifact_path
, db_dict
))
326 if not self
._authenticated
:
327 await self
._juju
_login
()
330 if ee_id
is None or len(ee_id
) == 0:
331 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
332 if artifact_path
is None or len(artifact_path
) == 0:
333 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
335 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
338 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
339 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
340 except Exception as e
:
341 raise N2VCBadArgumentsException(
342 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
346 # remove // in charm path
347 while artifact_path
.find('//') >= 0:
348 artifact_path
= artifact_path
.replace('//', '/')
351 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
352 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
353 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
355 if artifact_path
.startswith('/'):
356 full_path
= self
.fs
.path
+ artifact_path
358 full_path
= self
.fs
.path
+ '/' + artifact_path
361 application
, retries
= await self
._juju
_deploy
_charm
(
362 model_name
=model_name
,
363 application_name
=application_name
,
364 charm_path
=full_path
,
365 machine_id
=machine_id
,
367 progress_timeout
=progress_timeout
,
368 total_timeout
=total_timeout
370 except Exception as e
:
371 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
373 self
.info('Configuration sw installed')
375 async def get_ee_ssh_public__key(
379 progress_timeout
: float = None,
380 total_timeout
: float = None
383 self
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
385 if not self
._authenticated
:
386 await self
._juju
_login
()
389 if ee_id
is None or len(ee_id
) == 0:
390 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
392 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
395 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
396 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
397 except Exception as e
:
398 raise N2VCBadArgumentsException(
399 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
403 # try to execute ssh layer primitives (if exist):
409 # execute action: generate-ssh-key
411 output
, status
= await self
._juju
_execute
_action
(
412 model_name
=model_name
,
413 application_name
=application_name
,
414 action_name
='generate-ssh-key',
416 progress_timeout
=progress_timeout
,
417 total_timeout
=total_timeout
419 except Exception as e
:
420 self
.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e
))
422 # execute action: get-ssh-public-key
424 output
, status
= await self
._juju
_execute
_action
(
425 model_name
=model_name
,
426 application_name
=application_name
,
427 action_name
='get-ssh-public-key',
429 progress_timeout
=progress_timeout
,
430 total_timeout
=total_timeout
432 except Exception as e
:
433 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
437 # return public key if exists
440 async def add_relation(
448 self
.debug('adding new relation between {} and {}, endpoints: {}, {}'
449 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
451 if not self
._authenticated
:
452 await self
._juju
_login
()
454 # get model, application and machines
455 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
456 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
458 # model must be the same
459 if model_1
!= model_2
:
460 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
462 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
464 # add juju relations between two applications
466 self
._juju
_add
_relation
()
467 except Exception as e
:
468 message
= 'Error adding relation between {} and {}'.format(ee_id_1
, ee_id_2
)
470 raise N2VCException(message
=message
)
472 async def remove_relation(
475 if not self
._authenticated
:
476 await self
._juju
_login
()
478 self
.info('Method not implemented yet')
479 raise NotImplemented()
481 async def deregister_execution_environments(
484 if not self
._authenticated
:
485 await self
._juju
_login
()
487 self
.info('Method not implemented yet')
488 raise NotImplemented()
490 async def delete_namespace(
493 db_dict
: dict = None,
494 total_timeout
: float = None
496 self
.info('Deleting namespace={}'.format(namespace
))
498 if not self
._authenticated
:
499 await self
._juju
_login
()
502 if namespace
is None:
503 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
505 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
506 if ns_id
is not None:
507 self
.debug('Deleting model {}'.format(ns_id
))
509 await self
._juju
_destroy
_model
(
511 total_timeout
=total_timeout
513 except Exception as e
:
514 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
516 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
518 self
.info('Namespace {} deleted'.format(namespace
))
520 async def delete_execution_environment(
523 db_dict
: dict = None,
524 total_timeout
: float = None
526 self
.info('Deleting execution environment ee_id={}'.format(ee_id
))
528 if not self
._authenticated
:
529 await self
._juju
_login
()
533 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
535 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
537 # destroy the application
539 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
540 except Exception as e
:
541 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
542 .format(ee_id
, application_name
, e
))
544 # destroy the machine
546 await self
._juju
_destroy
_machine
(
547 model_name
=model_name
,
548 machine_id
=machine_id
,
549 total_timeout
=total_timeout
551 except Exception as e
:
552 raise N2VCException(message
='Error deleting execution environment {} (machine {}) : {}'
553 .format(ee_id
, machine_id
, e
))
555 self
.info('Execution environment {} deleted'.format(ee_id
))
557 async def exec_primitive(
562 db_dict
: dict = None,
563 progress_timeout
: float = None,
564 total_timeout
: float = None
567 self
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
569 if not self
._authenticated
:
570 await self
._juju
_login
()
573 if ee_id
is None or len(ee_id
) == 0:
574 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
575 if primitive_name
is None or len(primitive_name
) == 0:
576 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
577 if params_dict
is None:
581 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
583 raise N2VCBadArgumentsException(
584 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
588 if primitive_name
== 'config':
589 # Special case: config primitive
591 await self
._juju
_configure
_application
(
592 model_name
=model_name
,
593 application_name
=application_name
,
596 progress_timeout
=progress_timeout
,
597 total_timeout
=total_timeout
599 except Exception as e
:
600 self
.error('Error configuring juju application: {}'.format(e
))
601 raise N2VCExecutionException(
602 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
603 primitive_name
=primitive_name
608 output
, status
= await self
._juju
_execute
_action
(
609 model_name
=model_name
,
610 application_name
=application_name
,
611 action_name
=primitive_name
,
613 progress_timeout
=progress_timeout
,
614 total_timeout
=total_timeout
,
617 if status
== 'completed':
620 raise Exception('status is not completed: {}'.format(status
))
621 except Exception as e
:
622 self
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
623 raise N2VCExecutionException(
624 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
625 primitive_name
=primitive_name
628 async def disconnect(self
):
629 self
.info('closing juju N2VC...')
630 await self
._juju
_logout
()
633 ##################################################################################################
634 ########################################## P R I V A T E #########################################
635 ##################################################################################################
644 # write ee_id to database: _admin.deployed.VCA.x
646 the_table
= db_dict
['collection']
647 the_filter
= db_dict
['filter']
648 the_path
= db_dict
['path']
649 if not the_path
[-1] == '.':
650 the_path
= the_path
+ '.'
651 update_dict
= {the_path
+ 'ee_id': ee_id
}
652 self
.debug('Writing ee_id to database: {}'.format(the_path
))
656 update_dict
=update_dict
,
659 except Exception as e
:
660 self
.error('Error writing ee_id to database: {}'.format(e
))
665 application_name
: str,
669 Build an execution environment id form model, application and machine
671 :param application_name:
675 # id for the execution environment
676 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
679 def _get_ee_id_components(
681 ) -> (str, str, str):
683 Get model, application and machine components from an execution environment id
685 :return: model_name, application_name, machine_id
689 return None, None, None
691 # split components of id
692 parts
= ee_id
.split('.')
693 model_name
= parts
[0]
694 application_name
= parts
[1]
695 machine_id
= parts
[2]
696 return model_name
, application_name
, machine_id
698 def _get_application_name(self
, namespace
: str) -> str:
700 Build application name from namespace
702 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
705 # split namespace components
706 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
708 if vnf_id
is None or len(vnf_id
) == 0:
711 vnf_id
= 'vnf-' + vnf_id
713 if vdu_id
is None or len(vdu_id
) == 0:
716 vdu_id
= '-vdu-' + vdu_id
718 if vdu_count
is None or len(vdu_count
) == 0:
721 vdu_count
= '-cnt-' + vdu_count
723 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
725 return N2VCJujuConnector
._format
_app
_name
(application_name
)
727 async def _juju_create_machine(
730 application_name
: str,
731 machine_id
: str = None,
732 db_dict
: dict = None,
733 progress_timeout
: float = None,
734 total_timeout
: float = None
737 self
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
739 # get juju model and observer (create model if needed)
740 model
= await self
._juju
_get
_model
(model_name
=model_name
)
741 observer
= self
.juju_observers
[model_name
]
743 # find machine id in model
745 if machine_id
is not None:
746 self
.debug('Finding existing machine id {} in model'.format(machine_id
))
747 # get juju existing machines in the model
748 existing_machines
= await model
.get_machines()
749 if machine_id
in existing_machines
:
750 self
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
751 machine
= model
.machines
[machine_id
]
754 self
.debug('Creating a new machine in juju...')
755 # machine does not exist, create it and wait for it
756 machine
= await model
.add_machine(
763 # register machine with observer
764 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
766 # id for the execution environment
767 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
768 model_name
=model_name
,
769 application_name
=application_name
,
770 machine_id
=str(machine
.entity_id
)
773 # write ee_id in database
774 self
._write
_ee
_id
_db
(
779 # wait for machine creation
780 await observer
.wait_for_machine(
781 machine_id
=str(machine
.entity_id
),
782 progress_timeout
=progress_timeout
,
783 total_timeout
=total_timeout
788 self
.debug('Reusing old machine pending')
790 # register machine with observer
791 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
793 # machine does exist, but it is in creation process (pending), wait for create finalisation
794 await observer
.wait_for_machine(
795 machine_id
=machine
.entity_id
,
796 progress_timeout
=progress_timeout
,
797 total_timeout
=total_timeout
)
799 self
.debug("Machine ready at " + str(machine
.dns_name
))
802 async def _juju_provision_machine(
807 private_key_path
: str,
808 db_dict
: dict = None,
809 progress_timeout
: float = None,
810 total_timeout
: float = None
813 self
.debug('provisioning machine. model: {}, hostname: {}'.format(model_name
, hostname
))
815 if not self
._authenticated
:
816 await self
._juju
_login
()
818 # get juju model and observer
819 model
= await self
._juju
_get
_model
(model_name
=model_name
)
820 observer
= self
.juju_observers
[model_name
]
822 spec
= 'ssh:{}@{}:{}'.format(username
, hostname
, private_key_path
)
823 self
.debug('provisioning machine {}'.format(spec
))
825 machine
= await model
.add_machine(spec
=spec
)
826 except Exception as e
:
829 traceback
.print_exc(file=sys
.stdout
)
833 # register machine with observer
834 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
836 # wait for machine creation
837 self
.debug('waiting for provision completed... {}'.format(machine
.entity_id
))
838 await observer
.wait_for_machine(
840 progress_timeout
=progress_timeout
,
841 total_timeout
=total_timeout
844 self
.debug("Machine provisioned {}".format(machine
.entity_id
))
847 async def _juju_deploy_charm(
850 application_name
: str,
854 progress_timeout
: float = None,
855 total_timeout
: float = None
856 ) -> (Application
, int):
858 # get juju model and observer
859 model
= await self
._juju
_get
_model
(model_name
=model_name
)
860 observer
= self
.juju_observers
[model_name
]
862 # check if application already exists
864 if application_name
in model
.applications
:
865 application
= model
.applications
[application_name
]
867 if application
is None:
869 # application does not exist, create it and wait for it
870 self
.debug('deploying application {} to machine {}, model {}'
871 .format(application_name
, machine_id
, model_name
))
872 self
.debug('charm: {}'.format(charm_path
))
873 application
= await model
.deploy(
874 entity_url
=charm_path
,
875 application_name
=application_name
,
882 # register application with observer
883 observer
.register_application(application
=application
, db_dict
=db_dict
)
885 self
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
886 retries
= await observer
.wait_for_application(
887 application_id
=application
.entity_id
,
888 progress_timeout
=progress_timeout
,
889 total_timeout
=total_timeout
)
890 self
.debug('application deployed')
894 # register application with observer
895 observer
.register_application(application
=application
, db_dict
=db_dict
)
897 # application already exists, but not finalised
898 self
.debug('application already exists, waiting for deployed...')
899 retries
= await observer
.wait_for_application(
900 application_id
=application
.entity_id
,
901 progress_timeout
=progress_timeout
,
902 total_timeout
=total_timeout
)
903 self
.debug('application deployed')
905 return application
, retries
907 async def _juju_execute_action(
910 application_name
: str,
913 progress_timeout
: float = None,
914 total_timeout
: float = None,
918 # get juju model and observer
919 model
= await self
._juju
_get
_model
(model_name
=model_name
)
920 observer
= self
.juju_observers
[model_name
]
922 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
924 self
.debug('trying to execute action {}'.format(action_name
))
925 unit
= application
.units
[0]
927 actions
= await application
.get_actions()
928 if action_name
in actions
:
929 self
.debug('executing action {} with params {}'.format(action_name
, kwargs
))
930 action
= await unit
.run_action(action_name
, **kwargs
)
932 # register action with observer
933 observer
.register_action(action
=action
, db_dict
=db_dict
)
935 self
.debug(' waiting for action completed or error...')
936 await observer
.wait_for_action(
937 action_id
=action
.entity_id
,
938 progress_timeout
=progress_timeout
,
939 total_timeout
=total_timeout
)
940 self
.debug('action completed with status: {}'.format(action
.status
))
941 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
942 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
943 if action
.entity_id
in status
:
944 status
= status
[action
.entity_id
]
947 return output
, status
949 raise N2VCExecutionException(
950 message
='Cannot execute action on charm',
951 primitive_name
=action_name
954 async def _juju_configure_application(
957 application_name
: str,
960 progress_timeout
: float = None,
961 total_timeout
: float = None
965 model
= await self
._juju
_get
_model
(model_name
=model_name
)
967 # get the application
968 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
970 self
.debug('configuring the application {} -> {}'.format(application_name
, config
))
971 res
= await application
.set_config(config
)
972 self
.debug('application {} configured. res={}'.format(application_name
, res
))
974 # Verify the config is set
975 new_conf
= await application
.get_config()
977 value
= new_conf
[key
]['value']
978 self
.debug(' {} = {}'.format(key
, value
))
979 if config
[key
] != value
:
981 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
984 # check if 'verify-ssh-credentials' action exists
985 unit
= application
.units
[0]
986 actions
= await application
.get_actions()
987 if 'verify-ssh-credentials' not in actions
:
988 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
991 # execute verify-credentials
994 for i
in range(num_retries
):
996 self
.debug('Executing action verify-ssh-credentials...')
997 output
, ok
= await self
._juju
_execute
_action
(
998 model_name
=model_name
,
999 application_name
=application_name
,
1000 action_name
='verify-ssh-credentials',
1002 progress_timeout
=progress_timeout
,
1003 total_timeout
=total_timeout
1005 self
.debug('Result: {}, output: {}'.format(ok
, output
))
1007 except Exception as e
:
1008 self
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1009 await asyncio
.sleep(retry_timeout
)
1011 self
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1014 async def _juju_get_application(
1017 application_name
: str
1019 """Get the deployed application."""
1021 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1023 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1025 if model
.applications
and application_name
in model
.applications
:
1026 return model
.applications
[application_name
]
1028 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1030 async def _juju_get_model(self
, model_name
: str) -> Model
:
1031 """ Get a model object from juju controller
1033 :param str model_name: name of the model
1034 :returns Model: model obtained from juju controller or Exception
1038 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1040 if model_name
in self
.juju_models
:
1041 return self
.juju_models
[model_name
]
1043 if self
._creating
_model
:
1044 self
.debug('Another coroutine is creating a model. Wait...')
1045 while self
._creating
_model
:
1046 # another coroutine is creating a model, wait
1047 await asyncio
.sleep(0.1)
1048 # retry (perhaps another coroutine has created the model meanwhile)
1049 if model_name
in self
.juju_models
:
1050 return self
.juju_models
[model_name
]
1053 self
._creating
_model
= True
1055 # get juju model names from juju
1056 model_list
= await self
.controller
.list_models()
1058 if model_name
not in model_list
:
1059 self
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1060 model
= await self
.controller
.add_model(
1061 model_name
=model_name
,
1062 config
={'authorized-keys': self
.public_key
}
1064 self
.info('New model created, name={}'.format(model_name
))
1066 self
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1067 model
= await self
.controller
.get_model(model_name
)
1068 self
.debug('Existing model in juju, name={}'.format(model_name
))
1070 self
.juju_models
[model_name
] = model
1071 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1074 except Exception as e
:
1075 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1077 raise N2VCException(msg
)
1079 self
._creating
_model
= False
1081 async def _juju_add_relation(
1084 application_name_1
: str,
1085 application_name_2
: str,
1090 self
.debug('adding relation')
1092 # get juju model and observer
1093 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1095 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1096 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1097 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1099 async def _juju_destroy_application(
1102 application_name
: str
1105 self
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1107 # get juju model and observer
1108 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1110 application
= model
.applications
.get(application_name
)
1112 await application
.destroy()
1114 self
.debug('Application not found: {}'.format(application_name
))
1116 async def _juju_destroy_machine(
1120 total_timeout
: float = None
1123 self
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1125 if total_timeout
is None:
1126 total_timeout
= 3600
1128 # get juju model and observer
1129 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1131 machines
= await model
.get_machines()
1132 if machine_id
in machines
:
1133 machine
= model
.machines
[machine_id
]
1134 await machine
.destroy(force
=True)
1136 end
= time
.time() + total_timeout
1137 # wait for machine removal
1138 machines
= await model
.get_machines()
1139 while machine_id
in machines
and time
.time() < end
:
1140 self
.debug('Waiting for machine {} is destroyed'.format(machine_id
))
1141 await asyncio
.sleep(0.5)
1142 machines
= await model
.get_machines()
1143 self
.debug('Machine destroyed: {}'.format(machine_id
))
1145 self
.debug('Machine not found: {}'.format(machine_id
))
1147 async def _juju_destroy_model(
1150 total_timeout
: float = None
1153 self
.debug('Destroying model {}'.format(model_name
))
1155 if total_timeout
is None:
1156 total_timeout
= 3600
1158 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1159 uuid
= model
.info
.uuid
1161 self
.debug('disconnecting model {}...'.format(model_name
))
1162 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1163 self
.juju_models
[model_name
] = None
1164 self
.juju_observers
[model_name
] = None
1166 self
.debug('destroying model {}...'.format(model_name
))
1167 await self
.controller
.destroy_model(uuid
)
1169 # wait for model is completely destroyed
1170 end
= time
.time() + total_timeout
1171 while time
.time() < end
:
1172 self
.debug('waiting for model is destroyed...')
1174 await self
.controller
.get_model(uuid
)
1176 self
.debug('model destroyed')
1178 await asyncio
.sleep(1.0)
1180 async def _juju_login(self
):
1181 """Connect to juju controller
1185 # if already authenticated, exit function
1186 if self
._authenticated
:
1189 # if connecting, wait for finish
1190 # another task could be trying to connect in parallel
1191 while self
._connecting
:
1192 await asyncio
.sleep(0.1)
1194 # double check after other task has finished
1195 if self
._authenticated
:
1199 self
._connecting
= True
1201 'connecting to juju controller: {} {}:{} ca_cert: {}'
1202 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1204 # Create controller object
1205 self
.controller
= Controller(loop
=self
.loop
)
1206 # Connect to controller
1207 await self
.controller
.connect(
1209 username
=self
.username
,
1210 password
=self
.secret
,
1213 self
._authenticated
= True
1214 self
.info('juju controller connected')
1215 except Exception as e
:
1216 message
= 'Exception connecting to juju: {}'.format(e
)
1218 raise N2VCConnectionException(
1223 self
._connecting
= False
1225 async def _juju_logout(self
):
1226 """Logout of the Juju controller."""
1227 if not self
._authenticated
:
1230 # disconnect all models
1231 for model_name
in self
.juju_models
:
1233 await self
._juju
_disconnect
_model
(model_name
)
1234 except Exception as e
:
1235 self
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1236 # continue with next model...
1238 self
.info("Disconnecting controller")
1240 await self
.controller
.disconnect()
1241 except Exception as e
:
1242 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1244 self
.controller
= None
1245 self
._authenticated
= False
1246 self
.info('disconnected')
1248 async def _juju_disconnect_model(
1252 self
.debug("Disconnecting model {}".format(model_name
))
1253 if model_name
in self
.juju_models
:
1254 await self
.juju_models
[model_name
].disconnect()
1255 self
.juju_models
[model_name
] = None
1256 self
.juju_observers
[model_name
] = None
1258 def _create_juju_public_key(self
):
1259 """Recreate the Juju public key on lcm container, if needed
1260 Certain libjuju commands expect to be run from the same machine as Juju
1261 is bootstrapped to. This method will write the public key to disk in
1262 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1265 # Make sure that we have a public key before writing to disk
1266 if self
.public_key
is None or len(self
.public_key
) == 0:
1267 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1268 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1269 if len(self
.public_key
) == 0:
1274 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1275 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1276 self
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1277 if not os
.path
.exists(pk_path
):
1278 # create path and write file
1279 os
.makedirs(pk_path
)
1280 with
open(file_path
, 'w') as f
:
1281 self
.debug('Creating juju public key file: {}'.format(file_path
))
1282 f
.write(self
.public_key
)
1284 self
.debug('juju public key file already exists: {}'.format(file_path
))
1287 def _format_model_name(name
: str) -> str:
1288 """Format the name of the model.
1290 Model names may only contain lowercase letters, digits and hyphens
1293 return name
.replace('_', '-').replace(' ', '-').lower()
1296 def _format_app_name(name
: str) -> str:
1297 """Format the name of the application (in order to assure valid application name).
1299 Application names have restrictions (run juju deploy --help):
1300 - contains lowercase letters 'a'-'z'
1301 - contains numbers '0'-'9'
1302 - contains hyphens '-'
1303 - starts with a lowercase letter
1304 - not two or more consecutive hyphens
1305 - after a hyphen, not a group with all numbers
1308 def all_numbers(s
: str) -> bool:
1314 new_name
= name
.replace('_', '-')
1315 new_name
= new_name
.replace(' ', '-')
1316 new_name
= new_name
.lower()
1317 while new_name
.find('--') >= 0:
1318 new_name
= new_name
.replace('--', '-')
1319 groups
= new_name
.split('-')
1321 # find 'all numbers' groups and prefix them with a letter
1323 for i
in range(len(groups
)):
1325 if all_numbers(group
):
1331 if app_name
[0].isdigit():
1332 app_name
= 'z' + app_name