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('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
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 Exception as e
:
708 self
.log
.error('Error writing ee_id to database: {}'.format(e
))
713 application_name
: str,
717 Build an execution environment id form model, application and machine
719 :param application_name:
723 # id for the execution environment
724 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
727 def _get_ee_id_components(
729 ) -> (str, str, str):
731 Get model, application and machine components from an execution environment id
733 :return: model_name, application_name, machine_id
737 return None, None, None
739 # split components of id
740 parts
= ee_id
.split('.')
741 model_name
= parts
[0]
742 application_name
= parts
[1]
743 machine_id
= parts
[2]
744 return model_name
, application_name
, machine_id
746 def _get_application_name(self
, namespace
: str) -> str:
748 Build application name from namespace
750 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
753 # TODO: Enforce the Juju 50-character application limit
755 # split namespace components
756 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
758 if vnf_id
is None or len(vnf_id
) == 0:
761 # Shorten the vnf_id to its last twelve characters
762 vnf_id
= 'vnf-' + vnf_id
[-12:]
764 if vdu_id
is None or len(vdu_id
) == 0:
767 # Shorten the vdu_id to its last twelve characters
768 vdu_id
= '-vdu-' + vdu_id
[-12:]
770 if vdu_count
is None or len(vdu_count
) == 0:
773 vdu_count
= '-cnt-' + vdu_count
775 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
777 return N2VCJujuConnector
._format
_app
_name
(application_name
)
779 async def _juju_create_machine(
782 application_name
: str,
783 machine_id
: str = None,
784 db_dict
: dict = None,
785 progress_timeout
: float = None,
786 total_timeout
: float = None
789 self
.log
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
791 # get juju model and observer (create model if needed)
792 model
= await self
._juju
_get
_model
(model_name
=model_name
)
793 observer
= self
.juju_observers
[model_name
]
795 # find machine id in model
797 if machine_id
is not None:
798 self
.log
.debug('Finding existing machine id {} in model'.format(machine_id
))
799 # get juju existing machines in the model
800 existing_machines
= await model
.get_machines()
801 if machine_id
in existing_machines
:
802 self
.log
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
803 machine
= model
.machines
[machine_id
]
806 self
.log
.debug('Creating a new machine in juju...')
807 # machine does not exist, create it and wait for it
808 machine
= await model
.add_machine(
815 # register machine with observer
816 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
818 # id for the execution environment
819 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
820 model_name
=model_name
,
821 application_name
=application_name
,
822 machine_id
=str(machine
.entity_id
)
825 # write ee_id in database
826 self
._write
_ee
_id
_db
(
831 # wait for machine creation
832 await observer
.wait_for_machine(
833 machine_id
=str(machine
.entity_id
),
834 progress_timeout
=progress_timeout
,
835 total_timeout
=total_timeout
840 self
.log
.debug('Reusing old machine pending')
842 # register machine with observer
843 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
845 # machine does exist, but it is in creation process (pending), wait for create finalisation
846 await observer
.wait_for_machine(
847 machine_id
=machine
.entity_id
,
848 progress_timeout
=progress_timeout
,
849 total_timeout
=total_timeout
)
851 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
854 async def _juju_provision_machine(
859 private_key_path
: str,
860 db_dict
: dict = None,
861 progress_timeout
: float = None,
862 total_timeout
: float = None
865 if not self
.api_proxy
:
866 msg
= 'Cannot provision machine: api_proxy is not defined'
867 self
.log
.error(msg
=msg
)
868 raise N2VCException(message
=msg
)
870 self
.log
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
872 if not self
._authenticated
:
873 await self
._juju
_login
()
875 # get juju model and observer
876 model
= await self
._juju
_get
_model
(model_name
=model_name
)
877 observer
= self
.juju_observers
[model_name
]
879 # TODO check if machine is already provisioned
880 machine_list
= await model
.get_machines()
882 provisioner
= SSHProvisioner(
885 private_key_path
=private_key_path
,
891 params
= provisioner
.provision_machine()
892 except Exception as ex
:
893 msg
= "Exception provisioning machine: {}".format(ex
)
895 raise N2VCException(message
=msg
)
897 params
.jobs
= ['JobHostUnits']
899 connection
= model
.connection()
901 # Submit the request.
902 self
.log
.debug("Adding machine to model")
903 client_facade
= client
.ClientFacade
.from_connection(connection
)
904 results
= await client_facade
.AddMachines(params
=[params
])
905 error
= results
.machines
[0].error
907 msg
= "Error adding machine: {}}".format(error
.message
)
908 self
.log
.error(msg
=msg
)
909 raise ValueError(msg
)
911 machine_id
= results
.machines
[0].machine
913 # Need to run this after AddMachines has been called,
914 # as we need the machine_id
915 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
916 asyncio
.ensure_future(provisioner
.install_agent(
917 connection
=connection
,
919 machine_id
=machine_id
,
923 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
926 machine_list
= await model
.get_machines()
927 if machine_id
in machine_list
:
928 self
.log
.debug('Machine {} found in model!'.format(machine_id
))
929 machine
= model
.machines
.get(machine_id
)
931 await asyncio
.sleep(2)
934 msg
= 'Machine {} not found in model'.format(machine_id
)
935 self
.log
.error(msg
=msg
)
938 # register machine with observer
939 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
941 # wait for machine creation
942 self
.log
.debug('waiting for provision finishes... {}'.format(machine_id
))
943 await observer
.wait_for_machine(
944 machine_id
=machine_id
,
945 progress_timeout
=progress_timeout
,
946 total_timeout
=total_timeout
949 self
.log
.debug("Machine provisioned {}".format(machine_id
))
953 async def _juju_deploy_charm(
956 application_name
: str,
960 progress_timeout
: float = None,
961 total_timeout
: float = None,
963 ) -> (Application
, int):
965 # get juju model and observer
966 model
= await self
._juju
_get
_model
(model_name
=model_name
)
967 observer
= self
.juju_observers
[model_name
]
969 # check if application already exists
971 if application_name
in model
.applications
:
972 application
= model
.applications
[application_name
]
974 if application
is None:
976 # application does not exist, create it and wait for it
977 self
.log
.debug('deploying application {} to machine {}, model {}'
978 .format(application_name
, machine_id
, model_name
))
979 self
.log
.debug('charm: {}'.format(charm_path
))
982 application
= await model
.deploy(
983 entity_url
=charm_path
,
984 application_name
=application_name
,
992 # register application with observer
993 observer
.register_application(application
=application
, db_dict
=db_dict
)
995 self
.log
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
996 retries
= await observer
.wait_for_application(
997 application_id
=application
.entity_id
,
998 progress_timeout
=progress_timeout
,
999 total_timeout
=total_timeout
)
1000 self
.log
.debug('application deployed')
1004 # register application with observer
1005 observer
.register_application(application
=application
, db_dict
=db_dict
)
1007 # application already exists, but not finalised
1008 self
.log
.debug('application already exists, waiting for deployed...')
1009 retries
= await observer
.wait_for_application(
1010 application_id
=application
.entity_id
,
1011 progress_timeout
=progress_timeout
,
1012 total_timeout
=total_timeout
)
1013 self
.log
.debug('application deployed')
1015 return application
, retries
1017 async def _juju_execute_action(
1020 application_name
: str,
1023 progress_timeout
: float = None,
1024 total_timeout
: float = None,
1028 # get juju model and observer
1029 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1030 observer
= self
.juju_observers
[model_name
]
1032 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1035 for u
in application
.units
:
1036 if await u
.is_leader_from_status():
1038 if unit
is not None:
1039 actions
= await application
.get_actions()
1040 if action_name
in actions
:
1041 self
.log
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1042 action
= await unit
.run_action(action_name
, **kwargs
)
1044 # register action with observer
1045 observer
.register_action(action
=action
, db_dict
=db_dict
)
1047 await observer
.wait_for_action(
1048 action_id
=action
.entity_id
,
1049 progress_timeout
=progress_timeout
,
1050 total_timeout
=total_timeout
)
1051 self
.log
.debug('action completed with status: {}'.format(action
.status
))
1052 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1053 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1054 if action
.entity_id
in status
:
1055 status
= status
[action
.entity_id
]
1058 return output
, status
1060 raise N2VCExecutionException(
1061 message
='Cannot execute action on charm',
1062 primitive_name
=action_name
1065 async def _juju_configure_application(
1068 application_name
: str,
1071 progress_timeout
: float = None,
1072 total_timeout
: float = None
1075 # get the application
1076 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1078 self
.log
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1079 res
= await application
.set_config(config
)
1080 self
.log
.debug('application {} configured. res={}'.format(application_name
, res
))
1082 # Verify the config is set
1083 new_conf
= await application
.get_config()
1085 value
= new_conf
[key
]['value']
1086 self
.log
.debug(' {} = {}'.format(key
, value
))
1087 if config
[key
] != value
:
1088 raise N2VCException(
1089 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1092 # check if 'verify-ssh-credentials' action exists
1093 # unit = application.units[0]
1094 actions
= await application
.get_actions()
1095 if 'verify-ssh-credentials' not in actions
:
1096 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1097 self
.log
.debug(msg
=msg
)
1100 # execute verify-credentials
1102 retry_timeout
= 15.0
1103 for i
in range(num_retries
):
1105 self
.log
.debug('Executing action verify-ssh-credentials...')
1106 output
, ok
= await self
._juju
_execute
_action
(
1107 model_name
=model_name
,
1108 application_name
=application_name
,
1109 action_name
='verify-ssh-credentials',
1111 progress_timeout
=progress_timeout
,
1112 total_timeout
=total_timeout
1114 self
.log
.debug('Result: {}, output: {}'.format(ok
, output
))
1116 except Exception as e
:
1117 self
.log
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1118 await asyncio
.sleep(retry_timeout
)
1120 self
.log
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1123 async def _juju_get_application(
1126 application_name
: str
1128 """Get the deployed application."""
1130 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1132 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1134 if model
.applications
and application_name
in model
.applications
:
1135 return model
.applications
[application_name
]
1137 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1139 async def _juju_get_model(self
, model_name
: str) -> Model
:
1140 """ Get a model object from juju controller
1141 If the model does not exits, it creates it.
1143 :param str model_name: name of the model
1144 :returns Model: model obtained from juju controller or Exception
1148 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1150 if model_name
in self
.juju_models
:
1151 return self
.juju_models
[model_name
]
1153 if self
._creating
_model
:
1154 self
.log
.debug('Another coroutine is creating a model. Wait...')
1155 while self
._creating
_model
:
1156 # another coroutine is creating a model, wait
1157 await asyncio
.sleep(0.1)
1158 # retry (perhaps another coroutine has created the model meanwhile)
1159 if model_name
in self
.juju_models
:
1160 return self
.juju_models
[model_name
]
1163 self
._creating
_model
= True
1165 # get juju model names from juju
1166 model_list
= await self
.controller
.list_models()
1168 if model_name
not in model_list
:
1169 self
.log
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1170 config_dict
= {'authorized-keys': self
.public_key
}
1172 config_dict
['apt-mirror'] = self
.apt_mirror
1173 if not self
.enable_os_upgrade
:
1174 config_dict
['enable-os-refresh-update'] = False
1175 config_dict
['enable-os-upgrade'] = False
1177 model
= await self
.controller
.add_model(
1178 model_name
=model_name
,
1180 cloud_name
=self
.cloud
,
1182 self
.log
.info('New model created, name={}'.format(model_name
))
1184 self
.log
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1185 model
= await self
.controller
.get_model(model_name
)
1186 self
.log
.debug('Existing model in juju, name={}'.format(model_name
))
1188 self
.juju_models
[model_name
] = model
1189 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1192 except Exception as e
:
1193 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1195 raise N2VCException(msg
)
1197 self
._creating
_model
= False
1199 async def _juju_add_relation(
1202 application_name_1
: str,
1203 application_name_2
: str,
1208 # get juju model and observer
1209 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1211 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1212 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1214 self
.log
.debug('adding relation: {} -> {}'.format(r1
, r2
))
1216 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1217 except JujuAPIError
as e
:
1218 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1219 # let the operation fail silently.
1220 if 'not found' in e
.message
:
1222 if 'already exists' in e
.message
:
1224 # another execption, raise it
1227 async def _juju_destroy_application(
1230 application_name
: str
1233 self
.log
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1235 # get juju model and observer
1236 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1237 observer
= self
.juju_observers
[model_name
]
1239 application
= model
.applications
.get(application_name
)
1241 observer
.unregister_application(application_name
)
1242 await application
.destroy()
1244 self
.log
.debug('Application not found: {}'.format(application_name
))
1246 async def _juju_destroy_machine(
1250 total_timeout
: float = None
1253 self
.log
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1255 if total_timeout
is None:
1256 total_timeout
= 3600
1258 # get juju model and observer
1259 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1260 observer
= self
.juju_observers
[model_name
]
1262 machines
= await model
.get_machines()
1263 if machine_id
in machines
:
1264 machine
= model
.machines
[machine_id
]
1265 observer
.unregister_machine(machine_id
)
1266 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1267 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1269 ].startswith("manual:"):
1270 self
.log
.debug("machine.destroy(force=True) started.")
1271 await machine
.destroy(force
=True)
1272 self
.log
.debug("machine.destroy(force=True) passed.")
1274 end
= time
.time() + total_timeout
1275 # wait for machine removal
1276 machines
= await model
.get_machines()
1277 while machine_id
in machines
and time
.time() < end
:
1278 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
1279 await asyncio
.sleep(0.5)
1280 machines
= await model
.get_machines()
1281 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1283 self
.log
.debug('Machine not found: {}'.format(machine_id
))
1285 async def _juju_destroy_model(
1288 total_timeout
: float = None
1291 self
.log
.debug('Destroying model {}'.format(model_name
))
1293 if total_timeout
is None:
1294 total_timeout
= 3600
1296 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1300 message
="Model {} does not exist".format(model_name
)
1303 uuid
= model
.info
.uuid
1305 # destroy applications
1306 for application_name
in model
.applications
:
1308 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
1309 except Exception as e
:
1311 "Error destroying application {} in model {}: {}".format(
1319 machines
= await model
.get_machines()
1320 for machine_id
in machines
:
1322 await self
._juju
_destroy
_machine
(model_name
=model_name
, machine_id
=machine_id
)
1323 except Exception as e
:
1324 # ignore exceptions destroying machine
1327 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1329 self
.log
.debug('destroying model {}...'.format(model_name
))
1330 await self
.controller
.destroy_model(uuid
)
1331 self
.log
.debug('model destroy requested {}'.format(model_name
))
1333 # wait for model is completely destroyed
1334 end
= time
.time() + total_timeout
1335 while time
.time() < end
:
1336 self
.log
.debug('Waiting for model is destroyed...')
1338 # await self.controller.get_model(uuid)
1339 models
= await self
.controller
.list_models()
1340 if model_name
not in models
:
1341 self
.log
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1343 except Exception as e
:
1345 await asyncio
.sleep(1.0)
1347 async def _juju_login(self
):
1348 """Connect to juju controller
1352 # if already authenticated, exit function
1353 if self
._authenticated
:
1356 # if connecting, wait for finish
1357 # another task could be trying to connect in parallel
1358 while self
._connecting
:
1359 await asyncio
.sleep(0.1)
1361 # double check after other task has finished
1362 if self
._authenticated
:
1366 self
._connecting
= True
1368 'connecting to juju controller: {} {}:{} ca_cert: {}'
1369 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1371 # Create controller object
1372 self
.controller
= Controller(loop
=self
.loop
)
1373 # Connect to controller
1374 await self
.controller
.connect(
1376 username
=self
.username
,
1377 password
=self
.secret
,
1380 self
._authenticated
= True
1381 self
.log
.info('juju controller connected')
1382 except Exception as e
:
1383 message
= 'Exception connecting to juju: {}'.format(e
)
1384 self
.log
.error(message
)
1385 raise N2VCConnectionException(
1390 self
._connecting
= False
1392 async def _juju_logout(self
):
1393 """Logout of the Juju controller."""
1394 if not self
._authenticated
:
1397 # disconnect all models
1398 for model_name
in self
.juju_models
:
1400 await self
._juju
_disconnect
_model
(model_name
)
1401 except Exception as e
:
1402 self
.log
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1403 # continue with next model...
1405 self
.log
.info("Disconnecting controller")
1407 await self
.controller
.disconnect()
1408 except Exception as e
:
1409 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1411 self
.controller
= None
1412 self
._authenticated
= False
1413 self
.log
.info('disconnected')
1415 async def _juju_disconnect_model(
1419 self
.log
.debug("Disconnecting model {}".format(model_name
))
1420 if model_name
in self
.juju_models
:
1421 await self
.juju_models
[model_name
].disconnect()
1422 self
.juju_models
[model_name
] = None
1423 self
.juju_observers
[model_name
] = None
1425 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1427 def _create_juju_public_key(self
):
1428 """Recreate the Juju public key on lcm container, if needed
1429 Certain libjuju commands expect to be run from the same machine as Juju
1430 is bootstrapped to. This method will write the public key to disk in
1431 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1434 # Make sure that we have a public key before writing to disk
1435 if self
.public_key
is None or len(self
.public_key
) == 0:
1436 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1437 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1438 if len(self
.public_key
) == 0:
1443 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1444 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1445 self
.log
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1446 if not os
.path
.exists(pk_path
):
1447 # create path and write file
1448 os
.makedirs(pk_path
)
1449 with
open(file_path
, 'w') as f
:
1450 self
.log
.debug('Creating juju public key file: {}'.format(file_path
))
1451 f
.write(self
.public_key
)
1453 self
.log
.debug('juju public key file already exists: {}'.format(file_path
))
1456 def _format_model_name(name
: str) -> str:
1457 """Format the name of the model.
1459 Model names may only contain lowercase letters, digits and hyphens
1462 return name
.replace('_', '-').replace(' ', '-').lower()
1465 def _format_app_name(name
: str) -> str:
1466 """Format the name of the application (in order to assure valid application name).
1468 Application names have restrictions (run juju deploy --help):
1469 - contains lowercase letters 'a'-'z'
1470 - contains numbers '0'-'9'
1471 - contains hyphens '-'
1472 - starts with a lowercase letter
1473 - not two or more consecutive hyphens
1474 - after a hyphen, not a group with all numbers
1477 def all_numbers(s
: str) -> bool:
1483 new_name
= name
.replace('_', '-')
1484 new_name
= new_name
.replace(' ', '-')
1485 new_name
= new_name
.lower()
1486 while new_name
.find('--') >= 0:
1487 new_name
= new_name
.replace('--', '-')
1488 groups
= new_name
.split('-')
1490 # find 'all numbers' groups and prefix them with a letter
1492 for i
in range(len(groups
)):
1494 if all_numbers(group
):
1500 if app_name
[0].isdigit():
1501 app_name
= 'z' + app_name