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
, N2VCNotFound
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('Skipping exception while executing action generate-ssh-key: {}'.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
)
458 raise N2VCException(msg
)
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
, e
)
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
561 except Exception as e
:
562 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
564 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
566 self
.log
.info('Namespace {} deleted'.format(namespace
))
568 async def delete_execution_environment(
571 db_dict
: dict = None,
572 total_timeout
: float = None
574 self
.log
.info('Deleting execution environment ee_id={}'.format(ee_id
))
576 if not self
._authenticated
:
577 await self
._juju
_login
()
581 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
583 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
585 # destroy the application
587 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
588 except Exception as e
:
589 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
590 .format(ee_id
, application_name
, e
))
592 # destroy the machine
594 # await self._juju_destroy_machine(
595 # model_name=model_name,
596 # machine_id=machine_id,
597 # total_timeout=total_timeout
599 # except Exception as e:
600 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
601 # .format(ee_id, machine_id, e))
603 self
.log
.info('Execution environment {} deleted'.format(ee_id
))
605 async def exec_primitive(
610 db_dict
: dict = None,
611 progress_timeout
: float = None,
612 total_timeout
: float = None
615 self
.log
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
617 if not self
._authenticated
:
618 await self
._juju
_login
()
621 if ee_id
is None or len(ee_id
) == 0:
622 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
623 if primitive_name
is None or len(primitive_name
) == 0:
624 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
625 if params_dict
is None:
629 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
631 raise N2VCBadArgumentsException(
632 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
636 if primitive_name
== 'config':
637 # Special case: config primitive
639 await self
._juju
_configure
_application
(
640 model_name
=model_name
,
641 application_name
=application_name
,
644 progress_timeout
=progress_timeout
,
645 total_timeout
=total_timeout
647 except Exception as e
:
648 self
.log
.error('Error configuring juju application: {}'.format(e
))
649 raise N2VCExecutionException(
650 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
651 primitive_name
=primitive_name
656 output
, status
= await self
._juju
_execute
_action
(
657 model_name
=model_name
,
658 application_name
=application_name
,
659 action_name
=primitive_name
,
661 progress_timeout
=progress_timeout
,
662 total_timeout
=total_timeout
,
665 if status
== 'completed':
668 raise Exception('status is not completed: {}'.format(status
))
669 except Exception as e
:
670 self
.log
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
671 raise N2VCExecutionException(
672 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
673 primitive_name
=primitive_name
676 async def disconnect(self
):
677 self
.log
.info('closing juju N2VC...')
678 await self
._juju
_logout
()
681 ##################################################################################################
682 ########################################## P R I V A T E #########################################
683 ##################################################################################################
692 # write ee_id to database: _admin.deployed.VCA.x
694 the_table
= db_dict
['collection']
695 the_filter
= db_dict
['filter']
696 the_path
= db_dict
['path']
697 if not the_path
[-1] == '.':
698 the_path
= the_path
+ '.'
699 update_dict
= {the_path
+ 'ee_id': ee_id
}
700 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
704 update_dict
=update_dict
,
707 except asyncio
.CancelledError
:
709 except Exception as e
:
710 self
.log
.error('Error writing ee_id to database: {}'.format(e
))
715 application_name
: str,
719 Build an execution environment id form model, application and machine
721 :param application_name:
725 # id for the execution environment
726 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
729 def _get_ee_id_components(
731 ) -> (str, str, str):
733 Get model, application and machine components from an execution environment id
735 :return: model_name, application_name, machine_id
739 return None, None, None
741 # split components of id
742 parts
= ee_id
.split('.')
743 model_name
= parts
[0]
744 application_name
= parts
[1]
745 machine_id
= parts
[2]
746 return model_name
, application_name
, machine_id
748 def _get_application_name(self
, namespace
: str) -> str:
750 Build application name from namespace
752 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
755 # TODO: Enforce the Juju 50-character application limit
757 # split namespace components
758 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
760 if vnf_id
is None or len(vnf_id
) == 0:
763 # Shorten the vnf_id to its last twelve characters
764 vnf_id
= 'vnf-' + vnf_id
[-12:]
766 if vdu_id
is None or len(vdu_id
) == 0:
769 # Shorten the vdu_id to its last twelve characters
770 vdu_id
= '-vdu-' + vdu_id
[-12:]
772 if vdu_count
is None or len(vdu_count
) == 0:
775 vdu_count
= '-cnt-' + vdu_count
777 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
779 return N2VCJujuConnector
._format
_app
_name
(application_name
)
781 async def _juju_create_machine(
784 application_name
: str,
785 machine_id
: str = None,
786 db_dict
: dict = None,
787 progress_timeout
: float = None,
788 total_timeout
: float = None
791 self
.log
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
793 # get juju model and observer (create model if needed)
794 model
= await self
._juju
_get
_model
(model_name
=model_name
)
795 observer
= self
.juju_observers
[model_name
]
797 # find machine id in model
799 if machine_id
is not None:
800 self
.log
.debug('Finding existing machine id {} in model'.format(machine_id
))
801 # get juju existing machines in the model
802 existing_machines
= await model
.get_machines()
803 if machine_id
in existing_machines
:
804 self
.log
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
805 machine
= model
.machines
[machine_id
]
808 self
.log
.debug('Creating a new machine in juju...')
809 # machine does not exist, create it and wait for it
810 machine
= await model
.add_machine(
817 # register machine with observer
818 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
820 # id for the execution environment
821 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
822 model_name
=model_name
,
823 application_name
=application_name
,
824 machine_id
=str(machine
.entity_id
)
827 # write ee_id in database
828 self
._write
_ee
_id
_db
(
833 # wait for machine creation
834 await observer
.wait_for_machine(
835 machine_id
=str(machine
.entity_id
),
836 progress_timeout
=progress_timeout
,
837 total_timeout
=total_timeout
842 self
.log
.debug('Reusing old machine pending')
844 # register machine with observer
845 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
847 # machine does exist, but it is in creation process (pending), wait for create finalisation
848 await observer
.wait_for_machine(
849 machine_id
=machine
.entity_id
,
850 progress_timeout
=progress_timeout
,
851 total_timeout
=total_timeout
)
853 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
856 async def _juju_provision_machine(
861 private_key_path
: str,
862 db_dict
: dict = None,
863 progress_timeout
: float = None,
864 total_timeout
: float = None
867 if not self
.api_proxy
:
868 msg
= 'Cannot provision machine: api_proxy is not defined'
869 self
.log
.error(msg
=msg
)
870 raise N2VCException(message
=msg
)
872 self
.log
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
874 if not self
._authenticated
:
875 await self
._juju
_login
()
877 # get juju model and observer
878 model
= await self
._juju
_get
_model
(model_name
=model_name
)
879 observer
= self
.juju_observers
[model_name
]
881 # TODO check if machine is already provisioned
882 machine_list
= await model
.get_machines()
884 provisioner
= SSHProvisioner(
887 private_key_path
=private_key_path
,
893 params
= provisioner
.provision_machine()
894 except Exception as ex
:
895 msg
= "Exception provisioning machine: {}".format(ex
)
897 raise N2VCException(message
=msg
)
899 params
.jobs
= ['JobHostUnits']
901 connection
= model
.connection()
903 # Submit the request.
904 self
.log
.debug("Adding machine to model")
905 client_facade
= client
.ClientFacade
.from_connection(connection
)
906 results
= await client_facade
.AddMachines(params
=[params
])
907 error
= results
.machines
[0].error
909 msg
= "Error adding machine: {}}".format(error
.message
)
910 self
.log
.error(msg
=msg
)
911 raise ValueError(msg
)
913 machine_id
= results
.machines
[0].machine
915 # Need to run this after AddMachines has been called,
916 # as we need the machine_id
917 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
918 asyncio
.ensure_future(provisioner
.install_agent(
919 connection
=connection
,
921 machine_id
=machine_id
,
925 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
928 machine_list
= await model
.get_machines()
929 if machine_id
in machine_list
:
930 self
.log
.debug('Machine {} found in model!'.format(machine_id
))
931 machine
= model
.machines
.get(machine_id
)
933 await asyncio
.sleep(2)
936 msg
= 'Machine {} not found in model'.format(machine_id
)
937 self
.log
.error(msg
=msg
)
940 # register machine with observer
941 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
943 # wait for machine creation
944 self
.log
.debug('waiting for provision finishes... {}'.format(machine_id
))
945 await observer
.wait_for_machine(
946 machine_id
=machine_id
,
947 progress_timeout
=progress_timeout
,
948 total_timeout
=total_timeout
951 self
.log
.debug("Machine provisioned {}".format(machine_id
))
955 async def _juju_deploy_charm(
958 application_name
: str,
962 progress_timeout
: float = None,
963 total_timeout
: float = None,
965 ) -> (Application
, int):
967 # get juju model and observer
968 model
= await self
._juju
_get
_model
(model_name
=model_name
)
969 observer
= self
.juju_observers
[model_name
]
971 # check if application already exists
973 if application_name
in model
.applications
:
974 application
= model
.applications
[application_name
]
976 if application
is None:
978 # application does not exist, create it and wait for it
979 self
.log
.debug('deploying application {} to machine {}, model {}'
980 .format(application_name
, machine_id
, model_name
))
981 self
.log
.debug('charm: {}'.format(charm_path
))
984 application
= await model
.deploy(
985 entity_url
=charm_path
,
986 application_name
=application_name
,
994 # register application with observer
995 observer
.register_application(application
=application
, db_dict
=db_dict
)
997 self
.log
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
998 retries
= await observer
.wait_for_application(
999 application_id
=application
.entity_id
,
1000 progress_timeout
=progress_timeout
,
1001 total_timeout
=total_timeout
)
1002 self
.log
.debug('application deployed')
1006 # register application with observer
1007 observer
.register_application(application
=application
, db_dict
=db_dict
)
1009 # application already exists, but not finalised
1010 self
.log
.debug('application already exists, waiting for deployed...')
1011 retries
= await observer
.wait_for_application(
1012 application_id
=application
.entity_id
,
1013 progress_timeout
=progress_timeout
,
1014 total_timeout
=total_timeout
)
1015 self
.log
.debug('application deployed')
1017 return application
, retries
1019 async def _juju_execute_action(
1022 application_name
: str,
1025 progress_timeout
: float = None,
1026 total_timeout
: float = None,
1030 # get juju model and observer
1031 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1032 observer
= self
.juju_observers
[model_name
]
1034 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1037 for u
in application
.units
:
1038 if await u
.is_leader_from_status():
1040 if unit
is not None:
1041 actions
= await application
.get_actions()
1042 if action_name
in actions
:
1043 self
.log
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1044 action
= await unit
.run_action(action_name
, **kwargs
)
1046 # register action with observer
1047 observer
.register_action(action
=action
, db_dict
=db_dict
)
1049 await observer
.wait_for_action(
1050 action_id
=action
.entity_id
,
1051 progress_timeout
=progress_timeout
,
1052 total_timeout
=total_timeout
)
1053 self
.log
.debug('action completed with status: {}'.format(action
.status
))
1054 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1055 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1056 if action
.entity_id
in status
:
1057 status
= status
[action
.entity_id
]
1060 return output
, status
1062 raise N2VCExecutionException(
1063 message
='Cannot execute action on charm',
1064 primitive_name
=action_name
1067 async def _juju_configure_application(
1070 application_name
: str,
1073 progress_timeout
: float = None,
1074 total_timeout
: float = None
1077 # get the application
1078 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1080 self
.log
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1081 res
= await application
.set_config(config
)
1082 self
.log
.debug('application {} configured. res={}'.format(application_name
, res
))
1084 # Verify the config is set
1085 new_conf
= await application
.get_config()
1087 value
= new_conf
[key
]['value']
1088 self
.log
.debug(' {} = {}'.format(key
, value
))
1089 if config
[key
] != value
:
1090 raise N2VCException(
1091 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1094 # check if 'verify-ssh-credentials' action exists
1095 # unit = application.units[0]
1096 actions
= await application
.get_actions()
1097 if 'verify-ssh-credentials' not in actions
:
1098 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1099 self
.log
.debug(msg
=msg
)
1102 # execute verify-credentials
1104 retry_timeout
= 15.0
1105 for i
in range(num_retries
):
1107 self
.log
.debug('Executing action verify-ssh-credentials...')
1108 output
, ok
= await self
._juju
_execute
_action
(
1109 model_name
=model_name
,
1110 application_name
=application_name
,
1111 action_name
='verify-ssh-credentials',
1113 progress_timeout
=progress_timeout
,
1114 total_timeout
=total_timeout
1116 self
.log
.debug('Result: {}, output: {}'.format(ok
, output
))
1118 except asyncio
.CancelledError
:
1120 except Exception as e
:
1121 self
.log
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1122 await asyncio
.sleep(retry_timeout
)
1124 self
.log
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1127 async def _juju_get_application(
1130 application_name
: str
1132 """Get the deployed application."""
1134 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1136 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1138 if model
.applications
and application_name
in model
.applications
:
1139 return model
.applications
[application_name
]
1141 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1143 async def _juju_get_model(self
, model_name
: str) -> Model
:
1144 """ Get a model object from juju controller
1145 If the model does not exits, it creates it.
1147 :param str model_name: name of the model
1148 :returns Model: model obtained from juju controller or Exception
1152 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1154 if model_name
in self
.juju_models
:
1155 return self
.juju_models
[model_name
]
1157 if self
._creating
_model
:
1158 self
.log
.debug('Another coroutine is creating a model. Wait...')
1159 while self
._creating
_model
:
1160 # another coroutine is creating a model, wait
1161 await asyncio
.sleep(0.1)
1162 # retry (perhaps another coroutine has created the model meanwhile)
1163 if model_name
in self
.juju_models
:
1164 return self
.juju_models
[model_name
]
1167 self
._creating
_model
= True
1169 # get juju model names from juju
1170 model_list
= await self
.controller
.list_models()
1172 if model_name
not in model_list
:
1173 self
.log
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1174 config_dict
= {'authorized-keys': self
.public_key
}
1176 config_dict
['apt-mirror'] = self
.apt_mirror
1177 if not self
.enable_os_upgrade
:
1178 config_dict
['enable-os-refresh-update'] = False
1179 config_dict
['enable-os-upgrade'] = False
1181 model
= await self
.controller
.add_model(
1182 model_name
=model_name
,
1184 cloud_name
=self
.cloud
,
1186 self
.log
.info('New model created, name={}'.format(model_name
))
1188 self
.log
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1189 model
= await self
.controller
.get_model(model_name
)
1190 self
.log
.debug('Existing model in juju, name={}'.format(model_name
))
1192 self
.juju_models
[model_name
] = model
1193 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1196 except Exception as e
:
1197 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1199 raise N2VCException(msg
)
1201 self
._creating
_model
= False
1203 async def _juju_add_relation(
1206 application_name_1
: str,
1207 application_name_2
: str,
1212 # get juju model and observer
1213 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1215 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1216 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1218 self
.log
.debug('adding relation: {} -> {}'.format(r1
, r2
))
1220 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1221 except JujuAPIError
as e
:
1222 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1223 # let the operation fail silently.
1224 if 'not found' in e
.message
:
1226 if 'already exists' in e
.message
:
1228 # another execption, raise it
1231 async def _juju_destroy_application(
1234 application_name
: str
1237 self
.log
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1239 # get juju model and observer
1240 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1241 observer
= self
.juju_observers
[model_name
]
1243 application
= model
.applications
.get(application_name
)
1245 observer
.unregister_application(application_name
)
1246 await application
.destroy()
1248 self
.log
.debug('Application not found: {}'.format(application_name
))
1250 async def _juju_destroy_machine(
1254 total_timeout
: float = None
1257 self
.log
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1259 if total_timeout
is None:
1260 total_timeout
= 3600
1262 # get juju model and observer
1263 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1264 observer
= self
.juju_observers
[model_name
]
1266 machines
= await model
.get_machines()
1267 if machine_id
in machines
:
1268 machine
= model
.machines
[machine_id
]
1269 observer
.unregister_machine(machine_id
)
1270 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1271 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1273 ].startswith("manual:"):
1274 self
.log
.debug("machine.destroy(force=True) started.")
1275 await machine
.destroy(force
=True)
1276 self
.log
.debug("machine.destroy(force=True) passed.")
1278 end
= time
.time() + total_timeout
1279 # wait for machine removal
1280 machines
= await model
.get_machines()
1281 while machine_id
in machines
and time
.time() < end
:
1282 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
1283 await asyncio
.sleep(0.5)
1284 machines
= await model
.get_machines()
1285 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1287 self
.log
.debug('Machine not found: {}'.format(machine_id
))
1289 async def _juju_destroy_model(
1292 total_timeout
: float = None
1295 self
.log
.debug('Destroying model {}'.format(model_name
))
1297 if total_timeout
is None:
1298 total_timeout
= 3600
1299 end
= time
.time() + total_timeout
1301 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1305 message
="Model {} does not exist".format(model_name
)
1308 uuid
= model
.info
.uuid
1310 # destroy applications
1311 for application_name
in model
.applications
:
1313 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
1314 except Exception as e
:
1316 "Error destroying application {} in model {}: {}".format(
1324 machines
= await model
.get_machines()
1325 for machine_id
in machines
:
1327 await self
._juju
_destroy
_machine
(model_name
=model_name
, machine_id
=machine_id
)
1328 except asyncio
.CancelledError
:
1330 except Exception as e
:
1331 # ignore exceptions destroying machine
1334 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1336 self
.log
.debug('destroying model {}...'.format(model_name
))
1337 await self
.controller
.destroy_model(uuid
)
1338 # self.log.debug('model destroy requested {}'.format(model_name))
1340 # wait for model is completely destroyed
1341 self
.log
.debug('Waiting for model {} to be destroyed...'.format(model_name
))
1343 while time
.time() < end
:
1345 # await self.controller.get_model(uuid)
1346 models
= await self
.controller
.list_models()
1347 if model_name
not in models
:
1348 self
.log
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1350 except asyncio
.CancelledError
:
1352 except Exception as e
:
1354 await asyncio
.sleep(5)
1355 raise N2VCException("Timeout waiting for model {} to be destroyed {}".format(model_name
, last_exception
))
1357 async def _juju_login(self
):
1358 """Connect to juju controller
1362 # if already authenticated, exit function
1363 if self
._authenticated
:
1366 # if connecting, wait for finish
1367 # another task could be trying to connect in parallel
1368 while self
._connecting
:
1369 await asyncio
.sleep(0.1)
1371 # double check after other task has finished
1372 if self
._authenticated
:
1376 self
._connecting
= True
1378 'connecting to juju controller: {} {}:{}{}'
1379 .format(self
.url
, self
.username
, self
.secret
[:8] + '...', ' with ca_cert' if self
.ca_cert
else ''))
1381 # Create controller object
1382 self
.controller
= Controller(loop
=self
.loop
)
1383 # Connect to controller
1384 await self
.controller
.connect(
1386 username
=self
.username
,
1387 password
=self
.secret
,
1390 self
._authenticated
= True
1391 self
.log
.info('juju controller connected')
1392 except Exception as e
:
1393 message
= 'Exception connecting to juju: {}'.format(e
)
1394 self
.log
.error(message
)
1395 raise N2VCConnectionException(
1400 self
._connecting
= False
1402 async def _juju_logout(self
):
1403 """Logout of the Juju controller."""
1404 if not self
._authenticated
:
1407 # disconnect all models
1408 for model_name
in self
.juju_models
:
1410 await self
._juju
_disconnect
_model
(model_name
)
1411 except Exception as e
:
1412 self
.log
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1413 # continue with next model...
1415 self
.log
.info("Disconnecting controller")
1417 await self
.controller
.disconnect()
1418 except Exception as e
:
1419 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1421 self
.controller
= None
1422 self
._authenticated
= False
1423 self
.log
.info('disconnected')
1425 async def _juju_disconnect_model(
1429 self
.log
.debug("Disconnecting model {}".format(model_name
))
1430 if model_name
in self
.juju_models
:
1431 await self
.juju_models
[model_name
].disconnect()
1432 self
.juju_models
[model_name
] = None
1433 self
.juju_observers
[model_name
] = None
1435 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1437 def _create_juju_public_key(self
):
1438 """Recreate the Juju public key on lcm container, if needed
1439 Certain libjuju commands expect to be run from the same machine as Juju
1440 is bootstrapped to. This method will write the public key to disk in
1441 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1444 # Make sure that we have a public key before writing to disk
1445 if self
.public_key
is None or len(self
.public_key
) == 0:
1446 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1447 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1448 if len(self
.public_key
) == 0:
1453 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1454 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1455 self
.log
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1456 if not os
.path
.exists(pk_path
):
1457 # create path and write file
1458 os
.makedirs(pk_path
)
1459 with
open(file_path
, 'w') as f
:
1460 self
.log
.debug('Creating juju public key file: {}'.format(file_path
))
1461 f
.write(self
.public_key
)
1463 self
.log
.debug('juju public key file already exists: {}'.format(file_path
))
1466 def _format_model_name(name
: str) -> str:
1467 """Format the name of the model.
1469 Model names may only contain lowercase letters, digits and hyphens
1472 return name
.replace('_', '-').replace(' ', '-').lower()
1475 def _format_app_name(name
: str) -> str:
1476 """Format the name of the application (in order to assure valid application name).
1478 Application names have restrictions (run juju deploy --help):
1479 - contains lowercase letters 'a'-'z'
1480 - contains numbers '0'-'9'
1481 - contains hyphens '-'
1482 - starts with a lowercase letter
1483 - not two or more consecutive hyphens
1484 - after a hyphen, not a group with all numbers
1487 def all_numbers(s
: str) -> bool:
1493 new_name
= name
.replace('_', '-')
1494 new_name
= new_name
.replace(' ', '-')
1495 new_name
= new_name
.lower()
1496 while new_name
.find('--') >= 0:
1497 new_name
= new_name
.replace('--', '-')
1498 groups
= new_name
.split('-')
1500 # find 'all numbers' groups and prefix them with a letter
1502 for i
in range(len(groups
)):
1504 if all_numbers(group
):
1510 if app_name
[0].isdigit():
1511 app_name
= 'z' + app_name