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
344 self
.log
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
345 .format(ee_id
, artifact_path
, db_dict
))
347 if not self
._authenticated
:
348 await self
._juju
_login
()
351 if ee_id
is None or len(ee_id
) == 0:
352 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
353 if artifact_path
is None or len(artifact_path
) == 0:
354 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
356 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
359 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
360 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
361 except Exception as e
:
362 raise N2VCBadArgumentsException(
363 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
367 # remove // in charm path
368 while artifact_path
.find('//') >= 0:
369 artifact_path
= artifact_path
.replace('//', '/')
372 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
373 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
374 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
376 if artifact_path
.startswith('/'):
377 full_path
= self
.fs
.path
+ artifact_path
379 full_path
= self
.fs
.path
+ '/' + artifact_path
382 application
, retries
= await self
._juju
_deploy
_charm
(
383 model_name
=model_name
,
384 application_name
=application_name
,
385 charm_path
=full_path
,
386 machine_id
=machine_id
,
388 progress_timeout
=progress_timeout
,
389 total_timeout
=total_timeout
391 except Exception as e
:
392 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
394 self
.log
.info('Configuration sw installed')
396 async def get_ee_ssh_public__key(
400 progress_timeout
: float = None,
401 total_timeout
: float = None
404 self
.log
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
406 if not self
._authenticated
:
407 await self
._juju
_login
()
410 if ee_id
is None or len(ee_id
) == 0:
411 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
413 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
416 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
417 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
418 except Exception as e
:
419 raise N2VCBadArgumentsException(
420 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
424 # try to execute ssh layer primitives (if exist):
430 # execute action: generate-ssh-key
432 output
, status
= await self
._juju
_execute
_action
(
433 model_name
=model_name
,
434 application_name
=application_name
,
435 action_name
='generate-ssh-key',
437 progress_timeout
=progress_timeout
,
438 total_timeout
=total_timeout
440 except Exception as e
:
441 self
.log
.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e
))
443 # execute action: get-ssh-public-key
445 output
, status
= await self
._juju
_execute
_action
(
446 model_name
=model_name
,
447 application_name
=application_name
,
448 action_name
='get-ssh-public-key',
450 progress_timeout
=progress_timeout
,
451 total_timeout
=total_timeout
453 except Exception as e
:
454 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
458 # return public key if exists
459 return output
["pubkey"] if "pubkey" in output
else output
461 async def add_relation(
469 self
.log
.debug('adding new relation between {} and {}, endpoints: {}, {}'
470 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
474 message
= 'EE 1 is mandatory'
475 self
.log
.error(message
)
476 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1'])
478 message
= 'EE 2 is mandatory'
479 self
.log
.error(message
)
480 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_2'])
482 message
= 'endpoint 1 is mandatory'
483 self
.log
.error(message
)
484 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_1'])
486 message
= 'endpoint 2 is mandatory'
487 self
.log
.error(message
)
488 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_2'])
490 if not self
._authenticated
:
491 await self
._juju
_login
()
493 # get the model, the applications and the machines from the ee_id's
494 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
495 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
497 # model must be the same
498 if model_1
!= model_2
:
499 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
500 self
.log
.error(message
)
501 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
503 # add juju relations between two applications
505 await self
._juju
_add
_relation
(
507 application_name_1
=app_1
,
508 application_name_2
=app_2
,
509 relation_1
=endpoint_1
,
510 relation_2
=endpoint_2
512 except Exception as e
:
513 message
= 'Error adding relation between {} and {}'.format(ee_id_1
, ee_id_2
)
514 self
.log
.error(message
)
515 raise N2VCException(message
=message
)
517 async def remove_relation(
520 if not self
._authenticated
:
521 await self
._juju
_login
()
523 self
.log
.info('Method not implemented yet')
524 raise NotImplemented()
526 async def deregister_execution_environments(
529 if not self
._authenticated
:
530 await self
._juju
_login
()
532 self
.log
.info('Method not implemented yet')
533 raise NotImplemented()
535 async def delete_namespace(
538 db_dict
: dict = None,
539 total_timeout
: float = None
541 self
.log
.info('Deleting namespace={}'.format(namespace
))
543 if not self
._authenticated
:
544 await self
._juju
_login
()
547 if namespace
is None:
548 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
550 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
551 if ns_id
is not None:
553 await self
._juju
_destroy
_model
(
555 total_timeout
=total_timeout
557 except Exception as e
:
558 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
560 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
562 self
.log
.info('Namespace {} deleted'.format(namespace
))
564 async def delete_execution_environment(
567 db_dict
: dict = None,
568 total_timeout
: float = None
570 self
.log
.info('Deleting execution environment ee_id={}'.format(ee_id
))
572 if not self
._authenticated
:
573 await self
._juju
_login
()
577 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
579 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
581 # destroy the application
583 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
584 except Exception as e
:
585 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
586 .format(ee_id
, application_name
, e
))
588 # destroy the machine
590 # await self._juju_destroy_machine(
591 # model_name=model_name,
592 # machine_id=machine_id,
593 # total_timeout=total_timeout
595 # except Exception as e:
596 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
597 # .format(ee_id, machine_id, e))
599 self
.log
.info('Execution environment {} deleted'.format(ee_id
))
601 async def exec_primitive(
606 db_dict
: dict = None,
607 progress_timeout
: float = None,
608 total_timeout
: float = None
611 self
.log
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
613 if not self
._authenticated
:
614 await self
._juju
_login
()
617 if ee_id
is None or len(ee_id
) == 0:
618 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
619 if primitive_name
is None or len(primitive_name
) == 0:
620 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
621 if params_dict
is None:
625 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
627 raise N2VCBadArgumentsException(
628 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
632 if primitive_name
== 'config':
633 # Special case: config primitive
635 await self
._juju
_configure
_application
(
636 model_name
=model_name
,
637 application_name
=application_name
,
640 progress_timeout
=progress_timeout
,
641 total_timeout
=total_timeout
643 except Exception as e
:
644 self
.log
.error('Error configuring juju application: {}'.format(e
))
645 raise N2VCExecutionException(
646 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
647 primitive_name
=primitive_name
652 output
, status
= await self
._juju
_execute
_action
(
653 model_name
=model_name
,
654 application_name
=application_name
,
655 action_name
=primitive_name
,
657 progress_timeout
=progress_timeout
,
658 total_timeout
=total_timeout
,
661 if status
== 'completed':
664 raise Exception('status is not completed: {}'.format(status
))
665 except Exception as e
:
666 self
.log
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
667 raise N2VCExecutionException(
668 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
669 primitive_name
=primitive_name
672 async def disconnect(self
):
673 self
.log
.info('closing juju N2VC...')
674 await self
._juju
_logout
()
677 ##################################################################################################
678 ########################################## P R I V A T E #########################################
679 ##################################################################################################
688 # write ee_id to database: _admin.deployed.VCA.x
690 the_table
= db_dict
['collection']
691 the_filter
= db_dict
['filter']
692 the_path
= db_dict
['path']
693 if not the_path
[-1] == '.':
694 the_path
= the_path
+ '.'
695 update_dict
= {the_path
+ 'ee_id': ee_id
}
696 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
700 update_dict
=update_dict
,
703 except Exception as e
:
704 self
.log
.error('Error writing ee_id to database: {}'.format(e
))
709 application_name
: str,
713 Build an execution environment id form model, application and machine
715 :param application_name:
719 # id for the execution environment
720 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
723 def _get_ee_id_components(
725 ) -> (str, str, str):
727 Get model, application and machine components from an execution environment id
729 :return: model_name, application_name, machine_id
733 return None, None, None
735 # split components of id
736 parts
= ee_id
.split('.')
737 model_name
= parts
[0]
738 application_name
= parts
[1]
739 machine_id
= parts
[2]
740 return model_name
, application_name
, machine_id
742 def _get_application_name(self
, namespace
: str) -> str:
744 Build application name from namespace
746 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
749 # TODO: Enforce the Juju 50-character application limit
751 # split namespace components
752 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
754 if vnf_id
is None or len(vnf_id
) == 0:
757 # Shorten the vnf_id to its last twelve characters
758 vnf_id
= 'vnf-' + vnf_id
[-12:]
760 if vdu_id
is None or len(vdu_id
) == 0:
763 # Shorten the vdu_id to its last twelve characters
764 vdu_id
= '-vdu-' + vdu_id
[-12:]
766 if vdu_count
is None or len(vdu_count
) == 0:
769 vdu_count
= '-cnt-' + vdu_count
771 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
773 return N2VCJujuConnector
._format
_app
_name
(application_name
)
775 async def _juju_create_machine(
778 application_name
: str,
779 machine_id
: str = None,
780 db_dict
: dict = None,
781 progress_timeout
: float = None,
782 total_timeout
: float = None
785 self
.log
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
787 # get juju model and observer (create model if needed)
788 model
= await self
._juju
_get
_model
(model_name
=model_name
)
789 observer
= self
.juju_observers
[model_name
]
791 # find machine id in model
793 if machine_id
is not None:
794 self
.log
.debug('Finding existing machine id {} in model'.format(machine_id
))
795 # get juju existing machines in the model
796 existing_machines
= await model
.get_machines()
797 if machine_id
in existing_machines
:
798 self
.log
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
799 machine
= model
.machines
[machine_id
]
802 self
.log
.debug('Creating a new machine in juju...')
803 # machine does not exist, create it and wait for it
804 machine
= await model
.add_machine(
811 # register machine with observer
812 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
814 # id for the execution environment
815 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
816 model_name
=model_name
,
817 application_name
=application_name
,
818 machine_id
=str(machine
.entity_id
)
821 # write ee_id in database
822 self
._write
_ee
_id
_db
(
827 # wait for machine creation
828 await observer
.wait_for_machine(
829 machine_id
=str(machine
.entity_id
),
830 progress_timeout
=progress_timeout
,
831 total_timeout
=total_timeout
836 self
.log
.debug('Reusing old machine pending')
838 # register machine with observer
839 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
841 # machine does exist, but it is in creation process (pending), wait for create finalisation
842 await observer
.wait_for_machine(
843 machine_id
=machine
.entity_id
,
844 progress_timeout
=progress_timeout
,
845 total_timeout
=total_timeout
)
847 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
850 async def _juju_provision_machine(
855 private_key_path
: str,
856 db_dict
: dict = None,
857 progress_timeout
: float = None,
858 total_timeout
: float = None
861 if not self
.api_proxy
:
862 msg
= 'Cannot provision machine: api_proxy is not defined'
863 self
.log
.error(msg
=msg
)
864 raise N2VCException(message
=msg
)
866 self
.log
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
868 if not self
._authenticated
:
869 await self
._juju
_login
()
871 # get juju model and observer
872 model
= await self
._juju
_get
_model
(model_name
=model_name
)
873 observer
= self
.juju_observers
[model_name
]
875 # TODO check if machine is already provisioned
876 machine_list
= await model
.get_machines()
878 provisioner
= SSHProvisioner(
881 private_key_path
=private_key_path
,
887 params
= provisioner
.provision_machine()
888 except Exception as ex
:
889 msg
= "Exception provisioning machine: {}".format(ex
)
891 raise N2VCException(message
=msg
)
893 params
.jobs
= ['JobHostUnits']
895 connection
= model
.connection()
897 # Submit the request.
898 self
.log
.debug("Adding machine to model")
899 client_facade
= client
.ClientFacade
.from_connection(connection
)
900 results
= await client_facade
.AddMachines(params
=[params
])
901 error
= results
.machines
[0].error
903 msg
= "Error adding machine: {}}".format(error
.message
)
904 self
.log
.error(msg
=msg
)
905 raise ValueError(msg
)
907 machine_id
= results
.machines
[0].machine
909 # Need to run this after AddMachines has been called,
910 # as we need the machine_id
911 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
912 asyncio
.ensure_future(provisioner
.install_agent(
913 connection
=connection
,
915 machine_id
=machine_id
,
919 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
922 machine_list
= await model
.get_machines()
923 if machine_id
in machine_list
:
924 self
.log
.debug('Machine {} found in model!'.format(machine_id
))
925 machine
= model
.machines
.get(machine_id
)
927 await asyncio
.sleep(2)
930 msg
= 'Machine {} not found in model'.format(machine_id
)
931 self
.log
.error(msg
=msg
)
934 # register machine with observer
935 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
937 # wait for machine creation
938 self
.log
.debug('waiting for provision finishes... {}'.format(machine_id
))
939 await observer
.wait_for_machine(
940 machine_id
=machine_id
,
941 progress_timeout
=progress_timeout
,
942 total_timeout
=total_timeout
945 self
.log
.debug("Machine provisioned {}".format(machine_id
))
949 async def _juju_deploy_charm(
952 application_name
: str,
956 progress_timeout
: float = None,
957 total_timeout
: float = None
958 ) -> (Application
, int):
960 # get juju model and observer
961 model
= await self
._juju
_get
_model
(model_name
=model_name
)
962 observer
= self
.juju_observers
[model_name
]
964 # check if application already exists
966 if application_name
in model
.applications
:
967 application
= model
.applications
[application_name
]
969 if application
is None:
971 # application does not exist, create it and wait for it
972 self
.log
.debug('deploying application {} to machine {}, model {}'
973 .format(application_name
, machine_id
, model_name
))
974 self
.log
.debug('charm: {}'.format(charm_path
))
977 application
= await model
.deploy(
978 entity_url
=charm_path
,
979 application_name
=application_name
,
986 # register application with observer
987 observer
.register_application(application
=application
, db_dict
=db_dict
)
989 self
.log
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
990 retries
= await observer
.wait_for_application(
991 application_id
=application
.entity_id
,
992 progress_timeout
=progress_timeout
,
993 total_timeout
=total_timeout
)
994 self
.log
.debug('application deployed')
998 # register application with observer
999 observer
.register_application(application
=application
, db_dict
=db_dict
)
1001 # application already exists, but not finalised
1002 self
.log
.debug('application already exists, waiting for deployed...')
1003 retries
= await observer
.wait_for_application(
1004 application_id
=application
.entity_id
,
1005 progress_timeout
=progress_timeout
,
1006 total_timeout
=total_timeout
)
1007 self
.log
.debug('application deployed')
1009 return application
, retries
1011 async def _juju_execute_action(
1014 application_name
: str,
1017 progress_timeout
: float = None,
1018 total_timeout
: float = None,
1022 # get juju model and observer
1023 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1024 observer
= self
.juju_observers
[model_name
]
1026 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1029 for u
in application
.units
:
1030 if await u
.is_leader_from_status():
1032 if unit
is not None:
1033 actions
= await application
.get_actions()
1034 if action_name
in actions
:
1035 self
.log
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1036 action
= await unit
.run_action(action_name
, **kwargs
)
1038 # register action with observer
1039 observer
.register_action(action
=action
, db_dict
=db_dict
)
1041 await observer
.wait_for_action(
1042 action_id
=action
.entity_id
,
1043 progress_timeout
=progress_timeout
,
1044 total_timeout
=total_timeout
)
1045 self
.log
.debug('action completed with status: {}'.format(action
.status
))
1046 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1047 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1048 if action
.entity_id
in status
:
1049 status
= status
[action
.entity_id
]
1052 return output
, status
1054 raise N2VCExecutionException(
1055 message
='Cannot execute action on charm',
1056 primitive_name
=action_name
1059 async def _juju_configure_application(
1062 application_name
: str,
1065 progress_timeout
: float = None,
1066 total_timeout
: float = None
1069 # get the application
1070 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1072 self
.log
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1073 res
= await application
.set_config(config
)
1074 self
.log
.debug('application {} configured. res={}'.format(application_name
, res
))
1076 # Verify the config is set
1077 new_conf
= await application
.get_config()
1079 value
= new_conf
[key
]['value']
1080 self
.log
.debug(' {} = {}'.format(key
, value
))
1081 if config
[key
] != value
:
1082 raise N2VCException(
1083 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1086 # check if 'verify-ssh-credentials' action exists
1087 # unit = application.units[0]
1088 actions
= await application
.get_actions()
1089 if 'verify-ssh-credentials' not in actions
:
1090 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1091 self
.log
.debug(msg
=msg
)
1094 # execute verify-credentials
1096 retry_timeout
= 15.0
1097 for i
in range(num_retries
):
1099 self
.log
.debug('Executing action verify-ssh-credentials...')
1100 output
, ok
= await self
._juju
_execute
_action
(
1101 model_name
=model_name
,
1102 application_name
=application_name
,
1103 action_name
='verify-ssh-credentials',
1105 progress_timeout
=progress_timeout
,
1106 total_timeout
=total_timeout
1108 self
.log
.debug('Result: {}, output: {}'.format(ok
, output
))
1110 except Exception as e
:
1111 self
.log
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1112 await asyncio
.sleep(retry_timeout
)
1114 self
.log
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1117 async def _juju_get_application(
1120 application_name
: str
1122 """Get the deployed application."""
1124 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1126 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1128 if model
.applications
and application_name
in model
.applications
:
1129 return model
.applications
[application_name
]
1131 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1133 async def _juju_get_model(self
, model_name
: str) -> Model
:
1134 """ Get a model object from juju controller
1135 If the model does not exits, it creates it.
1137 :param str model_name: name of the model
1138 :returns Model: model obtained from juju controller or Exception
1142 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1144 if model_name
in self
.juju_models
:
1145 return self
.juju_models
[model_name
]
1147 if self
._creating
_model
:
1148 self
.log
.debug('Another coroutine is creating a model. Wait...')
1149 while self
._creating
_model
:
1150 # another coroutine is creating a model, wait
1151 await asyncio
.sleep(0.1)
1152 # retry (perhaps another coroutine has created the model meanwhile)
1153 if model_name
in self
.juju_models
:
1154 return self
.juju_models
[model_name
]
1157 self
._creating
_model
= True
1159 # get juju model names from juju
1160 model_list
= await self
.controller
.list_models()
1162 if model_name
not in model_list
:
1163 self
.log
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1164 config_dict
= {'authorized-keys': self
.public_key
}
1166 config_dict
['apt-mirror'] = self
.apt_mirror
1167 if not self
.enable_os_upgrade
:
1168 config_dict
['enable-os-refresh-update'] = False
1169 config_dict
['enable-os-upgrade'] = False
1171 model
= await self
.controller
.add_model(
1172 model_name
=model_name
,
1174 cloud_name
=self
.cloud
,
1176 self
.log
.info('New model created, name={}'.format(model_name
))
1178 self
.log
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1179 model
= await self
.controller
.get_model(model_name
)
1180 self
.log
.debug('Existing model in juju, name={}'.format(model_name
))
1182 self
.juju_models
[model_name
] = model
1183 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1186 except Exception as e
:
1187 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1189 raise N2VCException(msg
)
1191 self
._creating
_model
= False
1193 async def _juju_add_relation(
1196 application_name_1
: str,
1197 application_name_2
: str,
1202 # get juju model and observer
1203 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1205 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1206 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1208 self
.log
.debug('adding relation: {} -> {}'.format(r1
, r2
))
1210 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1211 except JujuAPIError
as e
:
1212 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1213 # let the operation fail silently.
1214 if 'not found' in e
.message
:
1216 if 'already exists' in e
.message
:
1218 # another execption, raise it
1221 async def _juju_destroy_application(
1224 application_name
: str
1227 self
.log
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1229 # get juju model and observer
1230 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1231 observer
= self
.juju_observers
[model_name
]
1233 application
= model
.applications
.get(application_name
)
1235 observer
.unregister_application(application_name
)
1236 await application
.destroy()
1238 self
.log
.debug('Application not found: {}'.format(application_name
))
1240 async def _juju_destroy_machine(
1244 total_timeout
: float = None
1247 self
.log
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1249 if total_timeout
is None:
1250 total_timeout
= 3600
1252 # get juju model and observer
1253 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1254 observer
= self
.juju_observers
[model_name
]
1256 machines
= await model
.get_machines()
1257 if machine_id
in machines
:
1258 machine
= model
.machines
[machine_id
]
1259 observer
.unregister_machine(machine_id
)
1260 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1261 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1263 ].startswith("manual:"):
1264 self
.log
.debug("machine.destroy(force=True) started.")
1265 await machine
.destroy(force
=True)
1266 self
.log
.debug("machine.destroy(force=True) passed.")
1268 end
= time
.time() + total_timeout
1269 # wait for machine removal
1270 machines
= await model
.get_machines()
1271 while machine_id
in machines
and time
.time() < end
:
1272 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
1273 await asyncio
.sleep(0.5)
1274 machines
= await model
.get_machines()
1275 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1277 self
.log
.debug('Machine not found: {}'.format(machine_id
))
1279 async def _juju_destroy_model(
1282 total_timeout
: float = None
1285 self
.log
.debug('Destroying model {}'.format(model_name
))
1287 if total_timeout
is None:
1288 total_timeout
= 3600
1290 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1293 raise N2VCException(
1294 message
="Model {} does not exist".format(model_name
)
1297 uuid
= model
.info
.uuid
1299 # destroy applications
1300 for application_name
in model
.applications
:
1302 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
1303 except Exception as e
:
1305 "Error destroying application {} in model {}: {}".format(
1313 machines
= await model
.get_machines()
1314 for machine_id
in machines
:
1316 await self
._juju
_destroy
_machine
(model_name
=model_name
, machine_id
=machine_id
)
1317 except Exception as e
:
1318 # ignore exceptions destroying machine
1321 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1323 self
.log
.debug('destroying model {}...'.format(model_name
))
1324 await self
.controller
.destroy_model(uuid
)
1325 self
.log
.debug('model destroy requested {}'.format(model_name
))
1327 # wait for model is completely destroyed
1328 end
= time
.time() + total_timeout
1329 while time
.time() < end
:
1330 self
.log
.debug('Waiting for model is destroyed...')
1332 # await self.controller.get_model(uuid)
1333 models
= await self
.controller
.list_models()
1334 if model_name
not in models
:
1335 self
.log
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1337 except Exception as e
:
1339 await asyncio
.sleep(1.0)
1341 async def _juju_login(self
):
1342 """Connect to juju controller
1346 # if already authenticated, exit function
1347 if self
._authenticated
:
1350 # if connecting, wait for finish
1351 # another task could be trying to connect in parallel
1352 while self
._connecting
:
1353 await asyncio
.sleep(0.1)
1355 # double check after other task has finished
1356 if self
._authenticated
:
1360 self
._connecting
= True
1362 'connecting to juju controller: {} {}:{} ca_cert: {}'
1363 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1365 # Create controller object
1366 self
.controller
= Controller(loop
=self
.loop
)
1367 # Connect to controller
1368 await self
.controller
.connect(
1370 username
=self
.username
,
1371 password
=self
.secret
,
1374 self
._authenticated
= True
1375 self
.log
.info('juju controller connected')
1376 except Exception as e
:
1377 message
= 'Exception connecting to juju: {}'.format(e
)
1378 self
.log
.error(message
)
1379 raise N2VCConnectionException(
1384 self
._connecting
= False
1386 async def _juju_logout(self
):
1387 """Logout of the Juju controller."""
1388 if not self
._authenticated
:
1391 # disconnect all models
1392 for model_name
in self
.juju_models
:
1394 await self
._juju
_disconnect
_model
(model_name
)
1395 except Exception as e
:
1396 self
.log
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1397 # continue with next model...
1399 self
.log
.info("Disconnecting controller")
1401 await self
.controller
.disconnect()
1402 except Exception as e
:
1403 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1405 self
.controller
= None
1406 self
._authenticated
= False
1407 self
.log
.info('disconnected')
1409 async def _juju_disconnect_model(
1413 self
.log
.debug("Disconnecting model {}".format(model_name
))
1414 if model_name
in self
.juju_models
:
1415 await self
.juju_models
[model_name
].disconnect()
1416 self
.juju_models
[model_name
] = None
1417 self
.juju_observers
[model_name
] = None
1419 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1421 def _create_juju_public_key(self
):
1422 """Recreate the Juju public key on lcm container, if needed
1423 Certain libjuju commands expect to be run from the same machine as Juju
1424 is bootstrapped to. This method will write the public key to disk in
1425 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1428 # Make sure that we have a public key before writing to disk
1429 if self
.public_key
is None or len(self
.public_key
) == 0:
1430 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1431 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1432 if len(self
.public_key
) == 0:
1437 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1438 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1439 self
.log
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1440 if not os
.path
.exists(pk_path
):
1441 # create path and write file
1442 os
.makedirs(pk_path
)
1443 with
open(file_path
, 'w') as f
:
1444 self
.log
.debug('Creating juju public key file: {}'.format(file_path
))
1445 f
.write(self
.public_key
)
1447 self
.log
.debug('juju public key file already exists: {}'.format(file_path
))
1450 def _format_model_name(name
: str) -> str:
1451 """Format the name of the model.
1453 Model names may only contain lowercase letters, digits and hyphens
1456 return name
.replace('_', '-').replace(' ', '-').lower()
1459 def _format_app_name(name
: str) -> str:
1460 """Format the name of the application (in order to assure valid application name).
1462 Application names have restrictions (run juju deploy --help):
1463 - contains lowercase letters 'a'-'z'
1464 - contains numbers '0'-'9'
1465 - contains hyphens '-'
1466 - starts with a lowercase letter
1467 - not two or more consecutive hyphens
1468 - after a hyphen, not a group with all numbers
1471 def all_numbers(s
: str) -> bool:
1477 new_name
= name
.replace('_', '-')
1478 new_name
= new_name
.replace(' ', '-')
1479 new_name
= new_name
.lower()
1480 while new_name
.find('--') >= 0:
1481 new_name
= new_name
.replace('--', '-')
1482 groups
= new_name
.split('-')
1484 # find 'all numbers' groups and prefix them with a letter
1486 for i
in range(len(groups
)):
1488 if all_numbers(group
):
1494 if app_name
[0].isdigit():
1495 app_name
= 'z' + app_name