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
.n2vc_conn
import obj_to_dict
, obj_to_yaml
33 from n2vc
.exceptions \
34 import N2VCBadArgumentsException
, N2VCException
, N2VCConnectionException
, \
35 N2VCExecutionException
, N2VCInvalidCertificate
36 from n2vc
.juju_observer
import JujuModelObserver
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
46 from n2vc
.provisioner
import SSHProvisioner
49 class N2VCJujuConnector(N2VCConnector
):
52 ##################################################################################################
53 ########################################## P U B L I C ###########################################
54 ##################################################################################################
63 url
: str = '127.0.0.1:17070',
64 username
: str = 'admin',
65 vca_config
: dict = None,
68 """Initialize juju N2VC connector
71 # parent class constructor
72 N2VCConnector
.__init
__(
80 vca_config
=vca_config
,
81 on_update_db
=on_update_db
84 # silence websocket traffic log
85 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
86 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
87 logging
.getLogger('model').setLevel(logging
.WARN
)
89 self
.log
.info('Initializing N2VC juju connector...')
92 ##############################################################
94 ##############################################################
99 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
100 url_parts
= url
.split(':')
101 if len(url_parts
) != 2:
102 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url
), ['url'])
103 self
.hostname
= url_parts
[0]
105 self
.port
= int(url_parts
[1])
107 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url
), ['url'])
111 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
114 if vca_config
is None:
115 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
117 if 'secret' in vca_config
:
118 self
.secret
= vca_config
['secret']
120 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
122 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
123 # if exists, it will be written in lcm container: _create_juju_public_key()
124 if 'public_key' in vca_config
:
125 self
.public_key
= vca_config
['public_key']
127 self
.public_key
= None
129 # TODO: Verify ca_cert is valid before using. VCA will crash
130 # if the ca_cert isn't formatted correctly.
131 def base64_to_cacert(b64string
):
132 """Convert the base64-encoded string containing the VCA CACERT.
138 cacert
= base64
.b64decode(b64string
).decode("utf-8")
145 except binascii
.Error
as e
:
146 self
.log
.debug("Caught binascii.Error: {}".format(e
))
147 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
151 self
.ca_cert
= vca_config
.get('ca_cert')
153 self
.ca_cert
= base64_to_cacert(vca_config
['ca_cert'])
155 if 'api_proxy' in vca_config
:
156 self
.api_proxy
= vca_config
['api_proxy']
157 self
.log
.debug('api_proxy for native charms configured: {}'.format(self
.api_proxy
))
159 self
.warning('api_proxy is not configured. Support for native charms is disabled')
161 if 'enable_os_upgrade' in vca_config
:
162 self
.enable_os_upgrade
= vca_config
['enable_os_upgrade']
164 self
.enable_os_upgrade
= True
166 if 'apt_mirror' in vca_config
:
167 self
.apt_mirror
= vca_config
['apt_mirror']
169 self
.apt_mirror
= None
171 self
.cloud
= vca_config
.get('cloud')
172 self
.log
.debug('Arguments have been checked')
175 self
.controller
= None # it will be filled when connect to juju
176 self
.juju_models
= {} # model objects for every model_name
177 self
.juju_observers
= {} # model observers for every model_name
178 self
._connecting
= False # while connecting to juju (to avoid duplicate connections)
179 self
._authenticated
= False # it will be True when juju connection be stablished
180 self
._creating
_model
= False # True during model creation
182 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
183 self
._create
_juju
_public
_key
()
185 self
.log
.info('N2VC juju connector initialized')
187 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
189 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
191 if not self
._authenticated
:
192 await self
._juju
_login
()
194 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
195 # model name is ns_id
197 if model_name
is None:
198 msg
= 'Namespace {} not valid'.format(namespace
)
200 raise N2VCBadArgumentsException(msg
, ['namespace'])
202 # get juju model (create model if needed)
203 model
= await self
._juju
_get
_model
(model_name
=model_name
)
205 status
= await model
.get_status()
208 return obj_to_yaml(status
)
210 return obj_to_dict(status
)
212 async def create_execution_environment(
216 reuse_ee_id
: str = None,
217 progress_timeout
: float = None,
218 total_timeout
: float = None
221 self
.log
.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace
, reuse_ee_id
))
223 if not self
._authenticated
:
224 await self
._juju
_login
()
228 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=reuse_ee_id
)
230 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
231 # model name is ns_id
234 application_name
= self
._get
_application
_name
(namespace
=namespace
)
236 self
.log
.debug('model name: {}, application name: {}, machine_id: {}'
237 .format(model_name
, application_name
, machine_id
))
239 # create or reuse a new juju machine
241 machine
= await self
._juju
_create
_machine
(
242 model_name
=model_name
,
243 application_name
=application_name
,
244 machine_id
=machine_id
,
246 progress_timeout
=progress_timeout
,
247 total_timeout
=total_timeout
249 except Exception as e
:
250 message
= 'Error creating machine on juju: {}'.format(e
)
251 self
.log
.error(message
)
252 raise N2VCException(message
=message
)
254 # id for the execution environment
255 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
256 model_name
=model_name
,
257 application_name
=application_name
,
258 machine_id
=str(machine
.entity_id
)
260 self
.log
.debug('ee_id: {}'.format(ee_id
))
262 # new machine credentials
264 credentials
['hostname'] = machine
.dns_name
266 self
.log
.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id
, credentials
))
268 return ee_id
, credentials
270 async def register_execution_environment(
275 progress_timeout
: float = None,
276 total_timeout
: float = None
279 if not self
._authenticated
:
280 await self
._juju
_login
()
282 self
.log
.info('Registering execution environment. namespace={}, credentials={}'.format(namespace
, credentials
))
284 if credentials
is None:
285 raise N2VCBadArgumentsException(message
='credentials are mandatory', bad_args
=['credentials'])
286 if credentials
.get('hostname'):
287 hostname
= credentials
['hostname']
289 raise N2VCBadArgumentsException(message
='hostname is mandatory', bad_args
=['credentials.hostname'])
290 if credentials
.get('username'):
291 username
= credentials
['username']
293 raise N2VCBadArgumentsException(message
='username is mandatory', bad_args
=['credentials.username'])
294 if 'private_key_path' in credentials
:
295 private_key_path
= credentials
['private_key_path']
297 # if not passed as argument, use generated private key path
298 private_key_path
= self
.private_key_path
300 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
305 application_name
= self
._get
_application
_name
(namespace
=namespace
)
307 # register machine on juju
309 machine_id
= await self
._juju
_provision
_machine
(
310 model_name
=model_name
,
313 private_key_path
=private_key_path
,
315 progress_timeout
=progress_timeout
,
316 total_timeout
=total_timeout
318 except Exception as e
:
319 self
.log
.error('Error registering machine: {}'.format(e
))
320 raise N2VCException(message
='Error registering machine on juju: {}'.format(e
))
322 self
.log
.info('Machine registered: {}'.format(machine_id
))
324 # id for the execution environment
325 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
326 model_name
=model_name
,
327 application_name
=application_name
,
328 machine_id
=str(machine_id
)
331 self
.log
.info('Execution environment registered. ee_id: {}'.format(ee_id
))
335 async def install_configuration_sw(
340 progress_timeout
: float = None,
341 total_timeout
: float = None,
345 self
.log
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
346 .format(ee_id
, artifact_path
, db_dict
))
348 if not self
._authenticated
:
349 await self
._juju
_login
()
352 if ee_id
is None or len(ee_id
) == 0:
353 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
354 if artifact_path
is None or len(artifact_path
) == 0:
355 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
357 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
360 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
361 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
362 except Exception as e
:
363 raise N2VCBadArgumentsException(
364 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
368 # remove // in charm path
369 while artifact_path
.find('//') >= 0:
370 artifact_path
= artifact_path
.replace('//', '/')
373 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
374 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
375 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
377 if artifact_path
.startswith('/'):
378 full_path
= self
.fs
.path
+ artifact_path
380 full_path
= self
.fs
.path
+ '/' + artifact_path
383 application
, retries
= await self
._juju
_deploy
_charm
(
384 model_name
=model_name
,
385 application_name
=application_name
,
386 charm_path
=full_path
,
387 machine_id
=machine_id
,
389 progress_timeout
=progress_timeout
,
390 total_timeout
=total_timeout
,
393 except Exception as e
:
394 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
396 self
.log
.info('Configuration sw installed')
398 async def get_ee_ssh_public__key(
402 progress_timeout
: float = None,
403 total_timeout
: float = None
406 self
.log
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
408 if not self
._authenticated
:
409 await self
._juju
_login
()
412 if ee_id
is None or len(ee_id
) == 0:
413 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
415 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
418 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
419 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
420 except Exception as e
:
421 raise N2VCBadArgumentsException(
422 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
426 # try to execute ssh layer primitives (if exist):
432 # execute action: generate-ssh-key
434 output
, status
= await self
._juju
_execute
_action
(
435 model_name
=model_name
,
436 application_name
=application_name
,
437 action_name
='generate-ssh-key',
439 progress_timeout
=progress_timeout
,
440 total_timeout
=total_timeout
442 except Exception as e
:
443 self
.log
.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e
))
445 # execute action: get-ssh-public-key
447 output
, status
= await self
._juju
_execute
_action
(
448 model_name
=model_name
,
449 application_name
=application_name
,
450 action_name
='get-ssh-public-key',
452 progress_timeout
=progress_timeout
,
453 total_timeout
=total_timeout
455 except Exception as e
:
456 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
460 # return public key if exists
461 return output
["pubkey"] if "pubkey" in output
else output
463 async def add_relation(
471 self
.log
.debug('adding new relation between {} and {}, endpoints: {}, {}'
472 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
476 message
= 'EE 1 is mandatory'
477 self
.log
.error(message
)
478 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1'])
480 message
= 'EE 2 is mandatory'
481 self
.log
.error(message
)
482 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_2'])
484 message
= 'endpoint 1 is mandatory'
485 self
.log
.error(message
)
486 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_1'])
488 message
= 'endpoint 2 is mandatory'
489 self
.log
.error(message
)
490 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_2'])
492 if not self
._authenticated
:
493 await self
._juju
_login
()
495 # get the model, the applications and the machines from the ee_id's
496 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
497 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
499 # model must be the same
500 if model_1
!= model_2
:
501 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
502 self
.log
.error(message
)
503 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
505 # add juju relations between two applications
507 await self
._juju
_add
_relation
(
509 application_name_1
=app_1
,
510 application_name_2
=app_2
,
511 relation_1
=endpoint_1
,
512 relation_2
=endpoint_2
514 except Exception as e
:
515 message
= 'Error adding relation between {} and {}'.format(ee_id_1
, ee_id_2
)
516 self
.log
.error(message
)
517 raise N2VCException(message
=message
)
519 async def remove_relation(
522 if not self
._authenticated
:
523 await self
._juju
_login
()
525 self
.log
.info('Method not implemented yet')
526 raise NotImplemented()
528 async def deregister_execution_environments(
531 if not self
._authenticated
:
532 await self
._juju
_login
()
534 self
.log
.info('Method not implemented yet')
535 raise NotImplemented()
537 async def delete_namespace(
540 db_dict
: dict = None,
541 total_timeout
: float = None
543 self
.log
.info('Deleting namespace={}'.format(namespace
))
545 if not self
._authenticated
:
546 await self
._juju
_login
()
549 if namespace
is None:
550 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
552 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
553 if ns_id
is not None:
555 await self
._juju
_destroy
_model
(
557 total_timeout
=total_timeout
559 except Exception as e
:
560 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
562 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
564 self
.log
.info('Namespace {} deleted'.format(namespace
))
566 async def delete_execution_environment(
569 db_dict
: dict = None,
570 total_timeout
: float = None
572 self
.log
.info('Deleting execution environment ee_id={}'.format(ee_id
))
574 if not self
._authenticated
:
575 await self
._juju
_login
()
579 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
581 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
583 # destroy the application
585 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
586 except Exception as e
:
587 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
588 .format(ee_id
, application_name
, e
))
590 # destroy the machine
592 # await self._juju_destroy_machine(
593 # model_name=model_name,
594 # machine_id=machine_id,
595 # total_timeout=total_timeout
597 # except Exception as e:
598 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
599 # .format(ee_id, machine_id, e))
601 self
.log
.info('Execution environment {} deleted'.format(ee_id
))
603 async def exec_primitive(
608 db_dict
: dict = None,
609 progress_timeout
: float = None,
610 total_timeout
: float = None
613 self
.log
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
615 if not self
._authenticated
:
616 await self
._juju
_login
()
619 if ee_id
is None or len(ee_id
) == 0:
620 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
621 if primitive_name
is None or len(primitive_name
) == 0:
622 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
623 if params_dict
is None:
627 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
629 raise N2VCBadArgumentsException(
630 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
634 if primitive_name
== 'config':
635 # Special case: config primitive
637 await self
._juju
_configure
_application
(
638 model_name
=model_name
,
639 application_name
=application_name
,
642 progress_timeout
=progress_timeout
,
643 total_timeout
=total_timeout
645 except Exception as e
:
646 self
.log
.error('Error configuring juju application: {}'.format(e
))
647 raise N2VCExecutionException(
648 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
649 primitive_name
=primitive_name
654 output
, status
= await self
._juju
_execute
_action
(
655 model_name
=model_name
,
656 application_name
=application_name
,
657 action_name
=primitive_name
,
659 progress_timeout
=progress_timeout
,
660 total_timeout
=total_timeout
,
663 if status
== 'completed':
666 raise Exception('status is not completed: {}'.format(status
))
667 except Exception as e
:
668 self
.log
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
669 raise N2VCExecutionException(
670 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
671 primitive_name
=primitive_name
674 async def disconnect(self
):
675 self
.log
.info('closing juju N2VC...')
676 await self
._juju
_logout
()
679 ##################################################################################################
680 ########################################## P R I V A T E #########################################
681 ##################################################################################################
690 # write ee_id to database: _admin.deployed.VCA.x
692 the_table
= db_dict
['collection']
693 the_filter
= db_dict
['filter']
694 the_path
= db_dict
['path']
695 if not the_path
[-1] == '.':
696 the_path
= the_path
+ '.'
697 update_dict
= {the_path
+ 'ee_id': ee_id
}
698 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
702 update_dict
=update_dict
,
705 except Exception as e
:
706 self
.log
.error('Error writing ee_id to database: {}'.format(e
))
711 application_name
: str,
715 Build an execution environment id form model, application and machine
717 :param application_name:
721 # id for the execution environment
722 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
725 def _get_ee_id_components(
727 ) -> (str, str, str):
729 Get model, application and machine components from an execution environment id
731 :return: model_name, application_name, machine_id
735 return None, None, None
737 # split components of id
738 parts
= ee_id
.split('.')
739 model_name
= parts
[0]
740 application_name
= parts
[1]
741 machine_id
= parts
[2]
742 return model_name
, application_name
, machine_id
744 def _get_application_name(self
, namespace
: str) -> str:
746 Build application name from namespace
748 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
751 # TODO: Enforce the Juju 50-character application limit
753 # split namespace components
754 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
756 if vnf_id
is None or len(vnf_id
) == 0:
759 # Shorten the vnf_id to its last twelve characters
760 vnf_id
= 'vnf-' + vnf_id
[-12:]
762 if vdu_id
is None or len(vdu_id
) == 0:
765 # Shorten the vdu_id to its last twelve characters
766 vdu_id
= '-vdu-' + vdu_id
[-12:]
768 if vdu_count
is None or len(vdu_count
) == 0:
771 vdu_count
= '-cnt-' + vdu_count
773 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
775 return N2VCJujuConnector
._format
_app
_name
(application_name
)
777 async def _juju_create_machine(
780 application_name
: str,
781 machine_id
: str = None,
782 db_dict
: dict = None,
783 progress_timeout
: float = None,
784 total_timeout
: float = None
787 self
.log
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
789 # get juju model and observer (create model if needed)
790 model
= await self
._juju
_get
_model
(model_name
=model_name
)
791 observer
= self
.juju_observers
[model_name
]
793 # find machine id in model
795 if machine_id
is not None:
796 self
.log
.debug('Finding existing machine id {} in model'.format(machine_id
))
797 # get juju existing machines in the model
798 existing_machines
= await model
.get_machines()
799 if machine_id
in existing_machines
:
800 self
.log
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
801 machine
= model
.machines
[machine_id
]
804 self
.log
.debug('Creating a new machine in juju...')
805 # machine does not exist, create it and wait for it
806 machine
= await model
.add_machine(
813 # register machine with observer
814 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
816 # id for the execution environment
817 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
818 model_name
=model_name
,
819 application_name
=application_name
,
820 machine_id
=str(machine
.entity_id
)
823 # write ee_id in database
824 self
._write
_ee
_id
_db
(
829 # wait for machine creation
830 await observer
.wait_for_machine(
831 machine_id
=str(machine
.entity_id
),
832 progress_timeout
=progress_timeout
,
833 total_timeout
=total_timeout
838 self
.log
.debug('Reusing old machine pending')
840 # register machine with observer
841 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
843 # machine does exist, but it is in creation process (pending), wait for create finalisation
844 await observer
.wait_for_machine(
845 machine_id
=machine
.entity_id
,
846 progress_timeout
=progress_timeout
,
847 total_timeout
=total_timeout
)
849 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
852 async def _juju_provision_machine(
857 private_key_path
: str,
858 db_dict
: dict = None,
859 progress_timeout
: float = None,
860 total_timeout
: float = None
863 if not self
.api_proxy
:
864 msg
= 'Cannot provision machine: api_proxy is not defined'
865 self
.log
.error(msg
=msg
)
866 raise N2VCException(message
=msg
)
868 self
.log
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
870 if not self
._authenticated
:
871 await self
._juju
_login
()
873 # get juju model and observer
874 model
= await self
._juju
_get
_model
(model_name
=model_name
)
875 observer
= self
.juju_observers
[model_name
]
877 # TODO check if machine is already provisioned
878 machine_list
= await model
.get_machines()
880 provisioner
= SSHProvisioner(
883 private_key_path
=private_key_path
,
889 params
= provisioner
.provision_machine()
890 except Exception as ex
:
891 msg
= "Exception provisioning machine: {}".format(ex
)
893 raise N2VCException(message
=msg
)
895 params
.jobs
= ['JobHostUnits']
897 connection
= model
.connection()
899 # Submit the request.
900 self
.log
.debug("Adding machine to model")
901 client_facade
= client
.ClientFacade
.from_connection(connection
)
902 results
= await client_facade
.AddMachines(params
=[params
])
903 error
= results
.machines
[0].error
905 msg
= "Error adding machine: {}}".format(error
.message
)
906 self
.log
.error(msg
=msg
)
907 raise ValueError(msg
)
909 machine_id
= results
.machines
[0].machine
911 # Need to run this after AddMachines has been called,
912 # as we need the machine_id
913 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
914 asyncio
.ensure_future(provisioner
.install_agent(
915 connection
=connection
,
917 machine_id
=machine_id
,
921 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
924 machine_list
= await model
.get_machines()
925 if machine_id
in machine_list
:
926 self
.log
.debug('Machine {} found in model!'.format(machine_id
))
927 machine
= model
.machines
.get(machine_id
)
929 await asyncio
.sleep(2)
932 msg
= 'Machine {} not found in model'.format(machine_id
)
933 self
.log
.error(msg
=msg
)
936 # register machine with observer
937 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
939 # wait for machine creation
940 self
.log
.debug('waiting for provision finishes... {}'.format(machine_id
))
941 await observer
.wait_for_machine(
942 machine_id
=machine_id
,
943 progress_timeout
=progress_timeout
,
944 total_timeout
=total_timeout
947 self
.log
.debug("Machine provisioned {}".format(machine_id
))
951 async def _juju_deploy_charm(
954 application_name
: str,
958 progress_timeout
: float = None,
959 total_timeout
: float = None,
961 ) -> (Application
, int):
963 # get juju model and observer
964 model
= await self
._juju
_get
_model
(model_name
=model_name
)
965 observer
= self
.juju_observers
[model_name
]
967 # check if application already exists
969 if application_name
in model
.applications
:
970 application
= model
.applications
[application_name
]
972 if application
is None:
974 # application does not exist, create it and wait for it
975 self
.log
.debug('deploying application {} to machine {}, model {}'
976 .format(application_name
, machine_id
, model_name
))
977 self
.log
.debug('charm: {}'.format(charm_path
))
980 application
= await model
.deploy(
981 entity_url
=charm_path
,
982 application_name
=application_name
,
990 # register application with observer
991 observer
.register_application(application
=application
, db_dict
=db_dict
)
993 self
.log
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
994 retries
= await observer
.wait_for_application(
995 application_id
=application
.entity_id
,
996 progress_timeout
=progress_timeout
,
997 total_timeout
=total_timeout
)
998 self
.log
.debug('application deployed')
1002 # register application with observer
1003 observer
.register_application(application
=application
, db_dict
=db_dict
)
1005 # application already exists, but not finalised
1006 self
.log
.debug('application already exists, waiting for deployed...')
1007 retries
= await observer
.wait_for_application(
1008 application_id
=application
.entity_id
,
1009 progress_timeout
=progress_timeout
,
1010 total_timeout
=total_timeout
)
1011 self
.log
.debug('application deployed')
1013 return application
, retries
1015 async def _juju_execute_action(
1018 application_name
: str,
1021 progress_timeout
: float = None,
1022 total_timeout
: float = None,
1026 # get juju model and observer
1027 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1028 observer
= self
.juju_observers
[model_name
]
1030 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1033 for u
in application
.units
:
1034 if await u
.is_leader_from_status():
1036 if unit
is not None:
1037 actions
= await application
.get_actions()
1038 if action_name
in actions
:
1039 self
.log
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1040 action
= await unit
.run_action(action_name
, **kwargs
)
1042 # register action with observer
1043 observer
.register_action(action
=action
, db_dict
=db_dict
)
1045 await observer
.wait_for_action(
1046 action_id
=action
.entity_id
,
1047 progress_timeout
=progress_timeout
,
1048 total_timeout
=total_timeout
)
1049 self
.log
.debug('action completed with status: {}'.format(action
.status
))
1050 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1051 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1052 if action
.entity_id
in status
:
1053 status
= status
[action
.entity_id
]
1056 return output
, status
1058 raise N2VCExecutionException(
1059 message
='Cannot execute action on charm',
1060 primitive_name
=action_name
1063 async def _juju_configure_application(
1066 application_name
: str,
1069 progress_timeout
: float = None,
1070 total_timeout
: float = None
1073 # get the application
1074 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1076 self
.log
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1077 res
= await application
.set_config(config
)
1078 self
.log
.debug('application {} configured. res={}'.format(application_name
, res
))
1080 # Verify the config is set
1081 new_conf
= await application
.get_config()
1083 value
= new_conf
[key
]['value']
1084 self
.log
.debug(' {} = {}'.format(key
, value
))
1085 if config
[key
] != value
:
1086 raise N2VCException(
1087 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1090 # check if 'verify-ssh-credentials' action exists
1091 # unit = application.units[0]
1092 actions
= await application
.get_actions()
1093 if 'verify-ssh-credentials' not in actions
:
1094 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1095 self
.log
.debug(msg
=msg
)
1098 # execute verify-credentials
1100 retry_timeout
= 15.0
1101 for i
in range(num_retries
):
1103 self
.log
.debug('Executing action verify-ssh-credentials...')
1104 output
, ok
= await self
._juju
_execute
_action
(
1105 model_name
=model_name
,
1106 application_name
=application_name
,
1107 action_name
='verify-ssh-credentials',
1109 progress_timeout
=progress_timeout
,
1110 total_timeout
=total_timeout
1112 self
.log
.debug('Result: {}, output: {}'.format(ok
, output
))
1114 except Exception as e
:
1115 self
.log
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1116 await asyncio
.sleep(retry_timeout
)
1118 self
.log
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1121 async def _juju_get_application(
1124 application_name
: str
1126 """Get the deployed application."""
1128 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1130 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1132 if model
.applications
and application_name
in model
.applications
:
1133 return model
.applications
[application_name
]
1135 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1137 async def _juju_get_model(self
, model_name
: str) -> Model
:
1138 """ Get a model object from juju controller
1139 If the model does not exits, it creates it.
1141 :param str model_name: name of the model
1142 :returns Model: model obtained from juju controller or Exception
1146 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1148 if model_name
in self
.juju_models
:
1149 return self
.juju_models
[model_name
]
1151 if self
._creating
_model
:
1152 self
.log
.debug('Another coroutine is creating a model. Wait...')
1153 while self
._creating
_model
:
1154 # another coroutine is creating a model, wait
1155 await asyncio
.sleep(0.1)
1156 # retry (perhaps another coroutine has created the model meanwhile)
1157 if model_name
in self
.juju_models
:
1158 return self
.juju_models
[model_name
]
1161 self
._creating
_model
= True
1163 # get juju model names from juju
1164 model_list
= await self
.controller
.list_models()
1166 if model_name
not in model_list
:
1167 self
.log
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1168 config_dict
= {'authorized-keys': self
.public_key
}
1170 config_dict
['apt-mirror'] = self
.apt_mirror
1171 if not self
.enable_os_upgrade
:
1172 config_dict
['enable-os-refresh-update'] = False
1173 config_dict
['enable-os-upgrade'] = False
1175 model
= await self
.controller
.add_model(
1176 model_name
=model_name
,
1178 cloud_name
=self
.cloud
,
1180 self
.log
.info('New model created, name={}'.format(model_name
))
1182 self
.log
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1183 model
= await self
.controller
.get_model(model_name
)
1184 self
.log
.debug('Existing model in juju, name={}'.format(model_name
))
1186 self
.juju_models
[model_name
] = model
1187 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1190 except Exception as e
:
1191 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1193 raise N2VCException(msg
)
1195 self
._creating
_model
= False
1197 async def _juju_add_relation(
1200 application_name_1
: str,
1201 application_name_2
: str,
1206 # get juju model and observer
1207 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1209 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1210 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1212 self
.log
.debug('adding relation: {} -> {}'.format(r1
, r2
))
1214 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1215 except JujuAPIError
as e
:
1216 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1217 # let the operation fail silently.
1218 if 'not found' in e
.message
:
1220 if 'already exists' in e
.message
:
1222 # another execption, raise it
1225 async def _juju_destroy_application(
1228 application_name
: str
1231 self
.log
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1233 # get juju model and observer
1234 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1235 observer
= self
.juju_observers
[model_name
]
1237 application
= model
.applications
.get(application_name
)
1239 observer
.unregister_application(application_name
)
1240 await application
.destroy()
1242 self
.log
.debug('Application not found: {}'.format(application_name
))
1244 async def _juju_destroy_machine(
1248 total_timeout
: float = None
1251 self
.log
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1253 if total_timeout
is None:
1254 total_timeout
= 3600
1256 # get juju model and observer
1257 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1258 observer
= self
.juju_observers
[model_name
]
1260 machines
= await model
.get_machines()
1261 if machine_id
in machines
:
1262 machine
= model
.machines
[machine_id
]
1263 observer
.unregister_machine(machine_id
)
1264 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1265 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1267 ].startswith("manual:"):
1268 self
.log
.debug("machine.destroy(force=True) started.")
1269 await machine
.destroy(force
=True)
1270 self
.log
.debug("machine.destroy(force=True) passed.")
1272 end
= time
.time() + total_timeout
1273 # wait for machine removal
1274 machines
= await model
.get_machines()
1275 while machine_id
in machines
and time
.time() < end
:
1276 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
1277 await asyncio
.sleep(0.5)
1278 machines
= await model
.get_machines()
1279 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1281 self
.log
.debug('Machine not found: {}'.format(machine_id
))
1283 async def _juju_destroy_model(
1286 total_timeout
: float = None
1289 self
.log
.debug('Destroying model {}'.format(model_name
))
1291 if total_timeout
is None:
1292 total_timeout
= 3600
1294 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1297 raise N2VCException(
1298 message
="Model {} does not exist".format(model_name
)
1301 uuid
= model
.info
.uuid
1303 # destroy applications
1304 for application_name
in model
.applications
:
1306 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
1307 except Exception as e
:
1309 "Error destroying application {} in model {}: {}".format(
1317 machines
= await model
.get_machines()
1318 for machine_id
in machines
:
1320 await self
._juju
_destroy
_machine
(model_name
=model_name
, machine_id
=machine_id
)
1321 except Exception as e
:
1322 # ignore exceptions destroying machine
1325 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1327 self
.log
.debug('destroying model {}...'.format(model_name
))
1328 await self
.controller
.destroy_model(uuid
)
1329 self
.log
.debug('model destroy requested {}'.format(model_name
))
1331 # wait for model is completely destroyed
1332 end
= time
.time() + total_timeout
1333 while time
.time() < end
:
1334 self
.log
.debug('Waiting for model is destroyed...')
1336 # await self.controller.get_model(uuid)
1337 models
= await self
.controller
.list_models()
1338 if model_name
not in models
:
1339 self
.log
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1341 except Exception as e
:
1343 await asyncio
.sleep(1.0)
1345 async def _juju_login(self
):
1346 """Connect to juju controller
1350 # if already authenticated, exit function
1351 if self
._authenticated
:
1354 # if connecting, wait for finish
1355 # another task could be trying to connect in parallel
1356 while self
._connecting
:
1357 await asyncio
.sleep(0.1)
1359 # double check after other task has finished
1360 if self
._authenticated
:
1364 self
._connecting
= True
1366 'connecting to juju controller: {} {}:{} ca_cert: {}'
1367 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1369 # Create controller object
1370 self
.controller
= Controller(loop
=self
.loop
)
1371 # Connect to controller
1372 await self
.controller
.connect(
1374 username
=self
.username
,
1375 password
=self
.secret
,
1378 self
._authenticated
= True
1379 self
.log
.info('juju controller connected')
1380 except Exception as e
:
1381 message
= 'Exception connecting to juju: {}'.format(e
)
1382 self
.log
.error(message
)
1383 raise N2VCConnectionException(
1388 self
._connecting
= False
1390 async def _juju_logout(self
):
1391 """Logout of the Juju controller."""
1392 if not self
._authenticated
:
1395 # disconnect all models
1396 for model_name
in self
.juju_models
:
1398 await self
._juju
_disconnect
_model
(model_name
)
1399 except Exception as e
:
1400 self
.log
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1401 # continue with next model...
1403 self
.log
.info("Disconnecting controller")
1405 await self
.controller
.disconnect()
1406 except Exception as e
:
1407 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1409 self
.controller
= None
1410 self
._authenticated
= False
1411 self
.log
.info('disconnected')
1413 async def _juju_disconnect_model(
1417 self
.log
.debug("Disconnecting model {}".format(model_name
))
1418 if model_name
in self
.juju_models
:
1419 await self
.juju_models
[model_name
].disconnect()
1420 self
.juju_models
[model_name
] = None
1421 self
.juju_observers
[model_name
] = None
1423 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1425 def _create_juju_public_key(self
):
1426 """Recreate the Juju public key on lcm container, if needed
1427 Certain libjuju commands expect to be run from the same machine as Juju
1428 is bootstrapped to. This method will write the public key to disk in
1429 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1432 # Make sure that we have a public key before writing to disk
1433 if self
.public_key
is None or len(self
.public_key
) == 0:
1434 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1435 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1436 if len(self
.public_key
) == 0:
1441 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1442 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1443 self
.log
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1444 if not os
.path
.exists(pk_path
):
1445 # create path and write file
1446 os
.makedirs(pk_path
)
1447 with
open(file_path
, 'w') as f
:
1448 self
.log
.debug('Creating juju public key file: {}'.format(file_path
))
1449 f
.write(self
.public_key
)
1451 self
.log
.debug('juju public key file already exists: {}'.format(file_path
))
1454 def _format_model_name(name
: str) -> str:
1455 """Format the name of the model.
1457 Model names may only contain lowercase letters, digits and hyphens
1460 return name
.replace('_', '-').replace(' ', '-').lower()
1463 def _format_app_name(name
: str) -> str:
1464 """Format the name of the application (in order to assure valid application name).
1466 Application names have restrictions (run juju deploy --help):
1467 - contains lowercase letters 'a'-'z'
1468 - contains numbers '0'-'9'
1469 - contains hyphens '-'
1470 - starts with a lowercase letter
1471 - not two or more consecutive hyphens
1472 - after a hyphen, not a group with all numbers
1475 def all_numbers(s
: str) -> bool:
1481 new_name
= name
.replace('_', '-')
1482 new_name
= new_name
.replace(' ', '-')
1483 new_name
= new_name
.lower()
1484 while new_name
.find('--') >= 0:
1485 new_name
= new_name
.replace('--', '-')
1486 groups
= new_name
.split('-')
1488 # find 'all numbers' groups and prefix them with a letter
1490 for i
in range(len(groups
)):
1492 if all_numbers(group
):
1498 if app_name
[0].isdigit():
1499 app_name
= 'z' + app_name