2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
31 from n2vc
.n2vc_conn
import N2VCConnector
32 from n2vc
.exceptions \
33 import N2VCBadArgumentsException
, N2VCException
, N2VCConnectionException
, \
34 N2VCExecutionException
, N2VCInvalidCertificate
35 from n2vc
.juju_observer
import JujuModelObserver
37 from juju
.controller
import Controller
38 from juju
.model
import Model
39 from juju
.application
import Application
40 from juju
.action
import Action
41 from juju
.machine
import Machine
42 from juju
.client
import client
44 from n2vc
.provisioner
import SSHProvisioner
47 class N2VCJujuConnector(N2VCConnector
):
50 ##################################################################################################
51 ########################################## P U B L I C ###########################################
52 ##################################################################################################
61 url
: str = '127.0.0.1:17070',
62 username
: str = 'admin',
63 vca_config
: dict = None,
66 """Initialize juju N2VC connector
69 # parent class constructor
70 N2VCConnector
.__init
__(
78 vca_config
=vca_config
,
79 on_update_db
=on_update_db
82 # silence websocket traffic log
83 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
84 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
85 logging
.getLogger('model').setLevel(logging
.WARN
)
87 self
.info('Initializing N2VC juju connector...')
90 ##############################################################
92 ##############################################################
97 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
98 url_parts
= url
.split(':')
99 if len(url_parts
) != 2:
100 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url
), ['url'])
101 self
.hostname
= url_parts
[0]
103 self
.port
= int(url_parts
[1])
105 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url
), ['url'])
109 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
112 if vca_config
is None:
113 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
115 if 'secret' in vca_config
:
116 self
.secret
= vca_config
['secret']
118 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
120 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
121 # if exists, it will be written in lcm container: _create_juju_public_key()
122 if 'public_key' in vca_config
:
123 self
.public_key
= vca_config
['public_key']
125 self
.public_key
= None
127 # TODO: Verify ca_cert is valid before using. VCA will crash
128 # if the ca_cert isn't formatted correctly.
129 def base64_to_cacert(b64string
):
130 """Convert the base64-encoded string containing the VCA CACERT.
136 cacert
= base64
.b64decode(b64string
).decode("utf-8")
143 except binascii
.Error
as e
:
144 self
.debug("Caught binascii.Error: {}".format(e
))
145 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
149 self
.ca_cert
= vca_config
.get('ca_cert')
151 self
.ca_cert
= base64_to_cacert(vca_config
['ca_cert'])
153 if 'api_proxy' in vca_config
:
154 self
.api_proxy
= vca_config
['api_proxy']
155 self
.debug('api_proxy for native charms configured: {}'.format(self
.api_proxy
))
157 self
.warning('api_proxy is not configured. Support for native charms is disabled')
159 self
.debug('Arguments have been checked')
162 self
.controller
= None # it will be filled when connect to juju
163 self
.juju_models
= {} # model objects for every model_name
164 self
.juju_observers
= {} # model observers for every model_name
165 self
._connecting
= False # while connecting to juju (to avoid duplicate connections)
166 self
._authenticated
= False # it will be True when juju connection be stablished
167 self
._creating
_model
= False # True during model creation
169 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
170 self
._create
_juju
_public
_key
()
172 self
.info('N2VC juju connector initialized')
174 async def get_status(self
, namespace
: str):
175 self
.info('Getting NS status. namespace: {}'.format(namespace
))
177 if not self
._authenticated
:
178 await self
._juju
_login
()
180 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
181 # model name is ns_id
183 if model_name
is None:
184 msg
= 'Namespace {} not valid'.format(namespace
)
186 raise N2VCBadArgumentsException(msg
, ['namespace'])
188 # get juju model (create model if needed)
189 model
= await self
._juju
_get
_model
(model_name
=model_name
)
191 status
= await model
.get_status()
195 async def create_execution_environment(
199 reuse_ee_id
: str = None,
200 progress_timeout
: float = None,
201 total_timeout
: float = None
204 self
.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace
, reuse_ee_id
))
206 if not self
._authenticated
:
207 await self
._juju
_login
()
211 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=reuse_ee_id
)
213 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
214 # model name is ns_id
217 application_name
= self
._get
_application
_name
(namespace
=namespace
)
219 self
.debug('model name: {}, application name: {}, machine_id: {}'
220 .format(model_name
, application_name
, machine_id
))
222 # create or reuse a new juju machine
224 machine
= await self
._juju
_create
_machine
(
225 model_name
=model_name
,
226 application_name
=application_name
,
227 machine_id
=machine_id
,
229 progress_timeout
=progress_timeout
,
230 total_timeout
=total_timeout
232 except Exception as e
:
233 message
= 'Error creating machine on juju: {}'.format(e
)
235 raise N2VCException(message
=message
)
237 # id for the execution environment
238 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
239 model_name
=model_name
,
240 application_name
=application_name
,
241 machine_id
=str(machine
.entity_id
)
243 self
.debug('ee_id: {}'.format(ee_id
))
245 # new machine credentials
247 credentials
['hostname'] = machine
.dns_name
249 self
.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id
, credentials
))
251 return ee_id
, credentials
253 async def register_execution_environment(
258 progress_timeout
: float = None,
259 total_timeout
: float = None
262 if not self
._authenticated
:
263 await self
._juju
_login
()
265 self
.info('Registering execution environment. namespace={}, credentials={}'.format(namespace
, credentials
))
267 if credentials
is None:
268 raise N2VCBadArgumentsException(message
='credentials are mandatory', bad_args
=['credentials'])
269 if credentials
.get('hostname'):
270 hostname
= credentials
['hostname']
272 raise N2VCBadArgumentsException(message
='hostname is mandatory', bad_args
=['credentials.hostname'])
273 if credentials
.get('username'):
274 username
= credentials
['username']
276 raise N2VCBadArgumentsException(message
='username is mandatory', bad_args
=['credentials.username'])
277 if 'private_key_path' in credentials
:
278 private_key_path
= credentials
['private_key_path']
280 # if not passed as argument, use generated private key path
281 private_key_path
= self
.private_key_path
283 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
288 application_name
= self
._get
_application
_name
(namespace
=namespace
)
290 # register machine on juju
292 machine_id
= await self
._juju
_provision
_machine
(
293 model_name
=model_name
,
296 private_key_path
=private_key_path
,
298 progress_timeout
=progress_timeout
,
299 total_timeout
=total_timeout
301 except Exception as e
:
302 self
.error('Error registering machine: {}'.format(e
))
303 raise N2VCException(message
='Error registering machine on juju: {}'.format(e
))
305 self
.info('Machine registered: {}'.format(machine_id
))
307 # id for the execution environment
308 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
309 model_name
=model_name
,
310 application_name
=application_name
,
311 machine_id
=str(machine_id
)
314 self
.info('Execution environment registered. ee_id: {}'.format(ee_id
))
318 async def install_configuration_sw(
323 progress_timeout
: float = None,
324 total_timeout
: float = None
327 self
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
328 .format(ee_id
, artifact_path
, db_dict
))
330 if not self
._authenticated
:
331 await self
._juju
_login
()
334 if ee_id
is None or len(ee_id
) == 0:
335 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
336 if artifact_path
is None or len(artifact_path
) == 0:
337 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
339 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
342 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
343 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
344 except Exception as e
:
345 raise N2VCBadArgumentsException(
346 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
350 # remove // in charm path
351 while artifact_path
.find('//') >= 0:
352 artifact_path
= artifact_path
.replace('//', '/')
355 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
356 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
357 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
359 if artifact_path
.startswith('/'):
360 full_path
= self
.fs
.path
+ artifact_path
362 full_path
= self
.fs
.path
+ '/' + artifact_path
365 application
, retries
= await self
._juju
_deploy
_charm
(
366 model_name
=model_name
,
367 application_name
=application_name
,
368 charm_path
=full_path
,
369 machine_id
=machine_id
,
371 progress_timeout
=progress_timeout
,
372 total_timeout
=total_timeout
374 except Exception as e
:
375 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
377 self
.info('Configuration sw installed')
379 async def get_ee_ssh_public__key(
383 progress_timeout
: float = None,
384 total_timeout
: float = None
387 self
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
389 if not self
._authenticated
:
390 await self
._juju
_login
()
393 if ee_id
is None or len(ee_id
) == 0:
394 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
396 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
399 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
400 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
401 except Exception as e
:
402 raise N2VCBadArgumentsException(
403 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
407 # try to execute ssh layer primitives (if exist):
413 # execute action: generate-ssh-key
415 output
, status
= await self
._juju
_execute
_action
(
416 model_name
=model_name
,
417 application_name
=application_name
,
418 action_name
='generate-ssh-key',
420 progress_timeout
=progress_timeout
,
421 total_timeout
=total_timeout
423 except Exception as e
:
424 self
.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e
))
426 # execute action: get-ssh-public-key
428 output
, status
= await self
._juju
_execute
_action
(
429 model_name
=model_name
,
430 application_name
=application_name
,
431 action_name
='get-ssh-public-key',
433 progress_timeout
=progress_timeout
,
434 total_timeout
=total_timeout
436 except Exception as e
:
437 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
441 # return public key if exists
444 async def add_relation(
452 self
.debug('adding new relation between {} and {}, endpoints: {}, {}'
453 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
455 if not self
._authenticated
:
456 await self
._juju
_login
()
458 # get model, application and machines
459 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
460 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
462 # model must be the same
463 if model_1
!= model_2
:
464 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
466 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
468 # add juju relations between two applications
470 self
._juju
_add
_relation
()
471 except Exception as e
:
472 message
= 'Error adding relation between {} and {}'.format(ee_id_1
, ee_id_2
)
474 raise N2VCException(message
=message
)
476 async def remove_relation(
479 if not self
._authenticated
:
480 await self
._juju
_login
()
482 self
.info('Method not implemented yet')
483 raise NotImplemented()
485 async def deregister_execution_environments(
488 if not self
._authenticated
:
489 await self
._juju
_login
()
491 self
.info('Method not implemented yet')
492 raise NotImplemented()
494 async def delete_namespace(
497 db_dict
: dict = None,
498 total_timeout
: float = None
500 self
.info('Deleting namespace={}'.format(namespace
))
502 if not self
._authenticated
:
503 await self
._juju
_login
()
506 if namespace
is None:
507 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
509 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
510 if ns_id
is not None:
511 self
.debug('Deleting model {}'.format(ns_id
))
513 await self
._juju
_destroy
_model
(
515 total_timeout
=total_timeout
517 except Exception as e
:
518 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
520 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
522 self
.info('Namespace {} deleted'.format(namespace
))
524 async def delete_execution_environment(
527 db_dict
: dict = None,
528 total_timeout
: float = None
530 self
.info('Deleting execution environment ee_id={}'.format(ee_id
))
532 if not self
._authenticated
:
533 await self
._juju
_login
()
537 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
539 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
541 # destroy the application
543 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
544 except Exception as e
:
545 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
546 .format(ee_id
, application_name
, e
))
548 # destroy the machine
550 await self
._juju
_destroy
_machine
(
551 model_name
=model_name
,
552 machine_id
=machine_id
,
553 total_timeout
=total_timeout
555 except Exception as e
:
556 raise N2VCException(message
='Error deleting execution environment {} (machine {}) : {}'
557 .format(ee_id
, machine_id
, e
))
559 self
.info('Execution environment {} deleted'.format(ee_id
))
561 async def exec_primitive(
566 db_dict
: dict = None,
567 progress_timeout
: float = None,
568 total_timeout
: float = None
571 self
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
573 if not self
._authenticated
:
574 await self
._juju
_login
()
577 if ee_id
is None or len(ee_id
) == 0:
578 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
579 if primitive_name
is None or len(primitive_name
) == 0:
580 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
581 if params_dict
is None:
585 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
587 raise N2VCBadArgumentsException(
588 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
592 if primitive_name
== 'config':
593 # Special case: config primitive
595 await self
._juju
_configure
_application
(
596 model_name
=model_name
,
597 application_name
=application_name
,
600 progress_timeout
=progress_timeout
,
601 total_timeout
=total_timeout
603 except Exception as e
:
604 self
.error('Error configuring juju application: {}'.format(e
))
605 raise N2VCExecutionException(
606 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
607 primitive_name
=primitive_name
612 output
, status
= await self
._juju
_execute
_action
(
613 model_name
=model_name
,
614 application_name
=application_name
,
615 action_name
=primitive_name
,
617 progress_timeout
=progress_timeout
,
618 total_timeout
=total_timeout
,
621 if status
== 'completed':
624 raise Exception('status is not completed: {}'.format(status
))
625 except Exception as e
:
626 self
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
627 raise N2VCExecutionException(
628 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
629 primitive_name
=primitive_name
632 async def disconnect(self
):
633 self
.info('closing juju N2VC...')
634 await self
._juju
_logout
()
637 ##################################################################################################
638 ########################################## P R I V A T E #########################################
639 ##################################################################################################
648 # write ee_id to database: _admin.deployed.VCA.x
650 the_table
= db_dict
['collection']
651 the_filter
= db_dict
['filter']
652 the_path
= db_dict
['path']
653 if not the_path
[-1] == '.':
654 the_path
= the_path
+ '.'
655 update_dict
= {the_path
+ 'ee_id': ee_id
}
656 self
.debug('Writing ee_id to database: {}'.format(the_path
))
660 update_dict
=update_dict
,
663 except Exception as e
:
664 self
.error('Error writing ee_id to database: {}'.format(e
))
669 application_name
: str,
673 Build an execution environment id form model, application and machine
675 :param application_name:
679 # id for the execution environment
680 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
683 def _get_ee_id_components(
685 ) -> (str, str, str):
687 Get model, application and machine components from an execution environment id
689 :return: model_name, application_name, machine_id
693 return None, None, None
695 # split components of id
696 parts
= ee_id
.split('.')
697 model_name
= parts
[0]
698 application_name
= parts
[1]
699 machine_id
= parts
[2]
700 return model_name
, application_name
, machine_id
702 def _get_application_name(self
, namespace
: str) -> str:
704 Build application name from namespace
706 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
709 # TODO: Enforce the Juju 50-character application limit
711 # split namespace components
712 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
714 if vnf_id
is None or len(vnf_id
) == 0:
717 # Shorten the vnf_id to its last twelve characters
718 vnf_id
= 'vnf-' + vnf_id
[-12:]
720 if vdu_id
is None or len(vdu_id
) == 0:
723 # Shorten the vdu_id to its last twelve characters
724 vdu_id
= '-vdu-' + vdu_id
[-12:]
726 if vdu_count
is None or len(vdu_count
) == 0:
729 vdu_count
= '-cnt-' + vdu_count
731 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
733 return N2VCJujuConnector
._format
_app
_name
(application_name
)
735 async def _juju_create_machine(
738 application_name
: str,
739 machine_id
: str = None,
740 db_dict
: dict = None,
741 progress_timeout
: float = None,
742 total_timeout
: float = None
745 self
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
747 # get juju model and observer (create model if needed)
748 model
= await self
._juju
_get
_model
(model_name
=model_name
)
749 observer
= self
.juju_observers
[model_name
]
751 # find machine id in model
753 if machine_id
is not None:
754 self
.debug('Finding existing machine id {} in model'.format(machine_id
))
755 # get juju existing machines in the model
756 existing_machines
= await model
.get_machines()
757 if machine_id
in existing_machines
:
758 self
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
759 machine
= model
.machines
[machine_id
]
762 self
.debug('Creating a new machine in juju...')
763 # machine does not exist, create it and wait for it
764 machine
= await model
.add_machine(
771 # register machine with observer
772 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
774 # id for the execution environment
775 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
776 model_name
=model_name
,
777 application_name
=application_name
,
778 machine_id
=str(machine
.entity_id
)
781 # write ee_id in database
782 self
._write
_ee
_id
_db
(
787 # wait for machine creation
788 await observer
.wait_for_machine(
789 machine_id
=str(machine
.entity_id
),
790 progress_timeout
=progress_timeout
,
791 total_timeout
=total_timeout
796 self
.debug('Reusing old machine pending')
798 # register machine with observer
799 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
801 # machine does exist, but it is in creation process (pending), wait for create finalisation
802 await observer
.wait_for_machine(
803 machine_id
=machine
.entity_id
,
804 progress_timeout
=progress_timeout
,
805 total_timeout
=total_timeout
)
807 self
.debug("Machine ready at " + str(machine
.dns_name
))
810 async def _juju_provision_machine(
815 private_key_path
: str,
816 db_dict
: dict = None,
817 progress_timeout
: float = None,
818 total_timeout
: float = None
821 if not self
.api_proxy
:
822 msg
= 'Cannot provision machine: api_proxy is not defined'
824 raise N2VCException(message
=msg
)
826 self
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
828 if not self
._authenticated
:
829 await self
._juju
_login
()
831 # get juju model and observer
832 model
= await self
._juju
_get
_model
(model_name
=model_name
)
833 observer
= self
.juju_observers
[model_name
]
835 # TODO check if machine is already provisioned
836 machine_list
= await model
.get_machines()
838 provisioner
= SSHProvisioner(
841 private_key_path
=private_key_path
,
847 params
= provisioner
.provision_machine()
848 except Exception as ex
:
849 msg
= "Exception provisioning machine: {}".format(ex
)
851 raise N2VCException(message
=msg
)
853 params
.jobs
= ['JobHostUnits']
855 connection
= model
.connection()
857 # Submit the request.
858 self
.debug("Adding machine to model")
859 client_facade
= client
.ClientFacade
.from_connection(connection
)
860 results
= await client_facade
.AddMachines(params
=[params
])
861 error
= results
.machines
[0].error
863 msg
= "Error adding machine: {}}".format(error
.message
)
865 raise ValueError(msg
)
867 machine_id
= results
.machines
[0].machine
869 # Need to run this after AddMachines has been called,
870 # as we need the machine_id
871 self
.debug("Installing Juju agent into machine {}".format(machine_id
))
872 asyncio
.ensure_future(provisioner
.install_agent(
873 connection
=connection
,
875 machine_id
=machine_id
,
879 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
882 machine_list
= await model
.get_machines()
883 if machine_id
in machine_list
:
884 self
.debug('Machine {} found in model!'.format(machine_id
))
885 machine
= model
.machines
.get(machine_id
)
887 await asyncio
.sleep(2)
890 msg
= 'Machine {} not found in model'.format(machine_id
)
894 # register machine with observer
895 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
897 # wait for machine creation
898 self
.debug('waiting for provision finishes... {}'.format(machine_id
))
899 await observer
.wait_for_machine(
900 machine_id
=machine_id
,
901 progress_timeout
=progress_timeout
,
902 total_timeout
=total_timeout
905 self
.debug("Machine provisioned {}".format(machine_id
))
909 async def _juju_deploy_charm(
912 application_name
: str,
916 progress_timeout
: float = None,
917 total_timeout
: float = None
918 ) -> (Application
, int):
920 # get juju model and observer
921 model
= await self
._juju
_get
_model
(model_name
=model_name
)
922 observer
= self
.juju_observers
[model_name
]
924 # check if application already exists
926 if application_name
in model
.applications
:
927 application
= model
.applications
[application_name
]
929 if application
is None:
931 # application does not exist, create it and wait for it
932 self
.debug('deploying application {} to machine {}, model {}'
933 .format(application_name
, machine_id
, model_name
))
934 self
.debug('charm: {}'.format(charm_path
))
937 application
= await model
.deploy(
938 entity_url
=charm_path
,
939 application_name
=application_name
,
946 # register application with observer
947 observer
.register_application(application
=application
, db_dict
=db_dict
)
949 self
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
950 retries
= await observer
.wait_for_application(
951 application_id
=application
.entity_id
,
952 progress_timeout
=progress_timeout
,
953 total_timeout
=total_timeout
)
954 self
.debug('application deployed')
958 # register application with observer
959 observer
.register_application(application
=application
, db_dict
=db_dict
)
961 # application already exists, but not finalised
962 self
.debug('application already exists, waiting for deployed...')
963 retries
= await observer
.wait_for_application(
964 application_id
=application
.entity_id
,
965 progress_timeout
=progress_timeout
,
966 total_timeout
=total_timeout
)
967 self
.debug('application deployed')
969 return application
, retries
971 async def _juju_execute_action(
974 application_name
: str,
977 progress_timeout
: float = None,
978 total_timeout
: float = None,
982 # get juju model and observer
983 model
= await self
._juju
_get
_model
(model_name
=model_name
)
984 observer
= self
.juju_observers
[model_name
]
986 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
988 self
.debug('trying to execute action {}'.format(action_name
))
989 unit
= application
.units
[0]
991 actions
= await application
.get_actions()
992 if action_name
in actions
:
993 self
.debug('executing action {} with params {}'.format(action_name
, kwargs
))
994 action
= await unit
.run_action(action_name
, **kwargs
)
996 # register action with observer
997 observer
.register_action(action
=action
, db_dict
=db_dict
)
999 self
.debug(' waiting for action completed or error...')
1000 await observer
.wait_for_action(
1001 action_id
=action
.entity_id
,
1002 progress_timeout
=progress_timeout
,
1003 total_timeout
=total_timeout
)
1004 self
.debug('action completed with status: {}'.format(action
.status
))
1005 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1006 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1007 if action
.entity_id
in status
:
1008 status
= status
[action
.entity_id
]
1011 return output
, status
1013 raise N2VCExecutionException(
1014 message
='Cannot execute action on charm',
1015 primitive_name
=action_name
1018 async def _juju_configure_application(
1021 application_name
: str,
1024 progress_timeout
: float = None,
1025 total_timeout
: float = None
1028 # get the application
1029 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1031 self
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1032 res
= await application
.set_config(config
)
1033 self
.debug('application {} configured. res={}'.format(application_name
, res
))
1035 # Verify the config is set
1036 new_conf
= await application
.get_config()
1038 value
= new_conf
[key
]['value']
1039 self
.debug(' {} = {}'.format(key
, value
))
1040 if config
[key
] != value
:
1041 raise N2VCException(
1042 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1045 # check if 'verify-ssh-credentials' action exists
1046 # unit = application.units[0]
1047 actions
= await application
.get_actions()
1048 if 'verify-ssh-credentials' not in actions
:
1049 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1053 # execute verify-credentials
1055 retry_timeout
= 15.0
1056 for i
in range(num_retries
):
1058 self
.debug('Executing action verify-ssh-credentials...')
1059 output
, ok
= await self
._juju
_execute
_action
(
1060 model_name
=model_name
,
1061 application_name
=application_name
,
1062 action_name
='verify-ssh-credentials',
1064 progress_timeout
=progress_timeout
,
1065 total_timeout
=total_timeout
1067 self
.debug('Result: {}, output: {}'.format(ok
, output
))
1069 except Exception as e
:
1070 self
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1071 await asyncio
.sleep(retry_timeout
)
1073 self
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1076 async def _juju_get_application(
1079 application_name
: str
1081 """Get the deployed application."""
1083 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1085 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1087 if model
.applications
and application_name
in model
.applications
:
1088 return model
.applications
[application_name
]
1090 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1092 async def _juju_get_model(self
, model_name
: str) -> Model
:
1093 """ Get a model object from juju controller
1095 :param str model_name: name of the model
1096 :returns Model: model obtained from juju controller or Exception
1100 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1102 if model_name
in self
.juju_models
:
1103 return self
.juju_models
[model_name
]
1105 if self
._creating
_model
:
1106 self
.debug('Another coroutine is creating a model. Wait...')
1107 while self
._creating
_model
:
1108 # another coroutine is creating a model, wait
1109 await asyncio
.sleep(0.1)
1110 # retry (perhaps another coroutine has created the model meanwhile)
1111 if model_name
in self
.juju_models
:
1112 return self
.juju_models
[model_name
]
1115 self
._creating
_model
= True
1117 # get juju model names from juju
1118 model_list
= await self
.controller
.list_models()
1120 if model_name
not in model_list
:
1121 self
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1122 model
= await self
.controller
.add_model(
1123 model_name
=model_name
,
1124 config
={'authorized-keys': self
.public_key
}
1126 self
.info('New model created, name={}'.format(model_name
))
1128 self
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1129 model
= await self
.controller
.get_model(model_name
)
1130 self
.debug('Existing model in juju, name={}'.format(model_name
))
1132 self
.juju_models
[model_name
] = model
1133 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1136 except Exception as e
:
1137 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1139 raise N2VCException(msg
)
1141 self
._creating
_model
= False
1143 async def _juju_add_relation(
1146 application_name_1
: str,
1147 application_name_2
: str,
1152 self
.debug('adding relation')
1154 # get juju model and observer
1155 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1157 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1158 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1159 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1161 async def _juju_destroy_application(
1164 application_name
: str
1167 self
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1169 # get juju model and observer
1170 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1172 application
= model
.applications
.get(application_name
)
1174 await application
.destroy()
1176 self
.debug('Application not found: {}'.format(application_name
))
1178 async def _juju_destroy_machine(
1182 total_timeout
: float = None
1185 self
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1187 if total_timeout
is None:
1188 total_timeout
= 3600
1190 # get juju model and observer
1191 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1193 machines
= await model
.get_machines()
1194 if machine_id
in machines
:
1195 machine
= model
.machines
[machine_id
]
1196 await machine
.destroy(force
=True)
1198 end
= time
.time() + total_timeout
1199 # wait for machine removal
1200 machines
= await model
.get_machines()
1201 while machine_id
in machines
and time
.time() < end
:
1202 self
.debug('Waiting for machine {} is destroyed'.format(machine_id
))
1203 await asyncio
.sleep(0.5)
1204 machines
= await model
.get_machines()
1205 self
.debug('Machine destroyed: {}'.format(machine_id
))
1207 self
.debug('Machine not found: {}'.format(machine_id
))
1209 async def _juju_destroy_model(
1212 total_timeout
: float = None
1215 self
.debug('Destroying model {}'.format(model_name
))
1217 if total_timeout
is None:
1218 total_timeout
= 3600
1220 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1221 uuid
= model
.info
.uuid
1223 self
.debug('disconnecting model {}...'.format(model_name
))
1224 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1225 self
.juju_models
[model_name
] = None
1226 self
.juju_observers
[model_name
] = None
1228 self
.debug('destroying model {}...'.format(model_name
))
1229 await self
.controller
.destroy_model(uuid
)
1231 # wait for model is completely destroyed
1232 end
= time
.time() + total_timeout
1233 while time
.time() < end
:
1234 self
.debug('waiting for model is destroyed...')
1236 await self
.controller
.get_model(uuid
)
1238 self
.debug('model destroyed')
1240 await asyncio
.sleep(1.0)
1242 async def _juju_login(self
):
1243 """Connect to juju controller
1247 # if already authenticated, exit function
1248 if self
._authenticated
:
1251 # if connecting, wait for finish
1252 # another task could be trying to connect in parallel
1253 while self
._connecting
:
1254 await asyncio
.sleep(0.1)
1256 # double check after other task has finished
1257 if self
._authenticated
:
1261 self
._connecting
= True
1263 'connecting to juju controller: {} {}:{} ca_cert: {}'
1264 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1266 # Create controller object
1267 self
.controller
= Controller(loop
=self
.loop
)
1268 # Connect to controller
1269 await self
.controller
.connect(
1271 username
=self
.username
,
1272 password
=self
.secret
,
1275 self
._authenticated
= True
1276 self
.info('juju controller connected')
1277 except Exception as e
:
1278 message
= 'Exception connecting to juju: {}'.format(e
)
1280 raise N2VCConnectionException(
1285 self
._connecting
= False
1287 async def _juju_logout(self
):
1288 """Logout of the Juju controller."""
1289 if not self
._authenticated
:
1292 # disconnect all models
1293 for model_name
in self
.juju_models
:
1295 await self
._juju
_disconnect
_model
(model_name
)
1296 except Exception as e
:
1297 self
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1298 # continue with next model...
1300 self
.info("Disconnecting controller")
1302 await self
.controller
.disconnect()
1303 except Exception as e
:
1304 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1306 self
.controller
= None
1307 self
._authenticated
= False
1308 self
.info('disconnected')
1310 async def _juju_disconnect_model(
1314 self
.debug("Disconnecting model {}".format(model_name
))
1315 if model_name
in self
.juju_models
:
1316 await self
.juju_models
[model_name
].disconnect()
1317 self
.juju_models
[model_name
] = None
1318 self
.juju_observers
[model_name
] = None
1320 def _create_juju_public_key(self
):
1321 """Recreate the Juju public key on lcm container, if needed
1322 Certain libjuju commands expect to be run from the same machine as Juju
1323 is bootstrapped to. This method will write the public key to disk in
1324 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1327 # Make sure that we have a public key before writing to disk
1328 if self
.public_key
is None or len(self
.public_key
) == 0:
1329 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1330 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1331 if len(self
.public_key
) == 0:
1336 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1337 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1338 self
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1339 if not os
.path
.exists(pk_path
):
1340 # create path and write file
1341 os
.makedirs(pk_path
)
1342 with
open(file_path
, 'w') as f
:
1343 self
.debug('Creating juju public key file: {}'.format(file_path
))
1344 f
.write(self
.public_key
)
1346 self
.debug('juju public key file already exists: {}'.format(file_path
))
1349 def _format_model_name(name
: str) -> str:
1350 """Format the name of the model.
1352 Model names may only contain lowercase letters, digits and hyphens
1355 return name
.replace('_', '-').replace(' ', '-').lower()
1358 def _format_app_name(name
: str) -> str:
1359 """Format the name of the application (in order to assure valid application name).
1361 Application names have restrictions (run juju deploy --help):
1362 - contains lowercase letters 'a'-'z'
1363 - contains numbers '0'-'9'
1364 - contains hyphens '-'
1365 - starts with a lowercase letter
1366 - not two or more consecutive hyphens
1367 - after a hyphen, not a group with all numbers
1370 def all_numbers(s
: str) -> bool:
1376 new_name
= name
.replace('_', '-')
1377 new_name
= new_name
.replace(' ', '-')
1378 new_name
= new_name
.lower()
1379 while new_name
.find('--') >= 0:
1380 new_name
= new_name
.replace('--', '-')
1381 groups
= new_name
.split('-')
1383 # find 'all numbers' groups and prefix them with a letter
1385 for i
in range(len(groups
)):
1387 if all_numbers(group
):
1393 if app_name
[0].isdigit():
1394 app_name
= 'z' + app_name