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
45 from n2vc
.provisioner
import SSHProvisioner
48 class N2VCJujuConnector(N2VCConnector
):
51 ##################################################################################################
52 ########################################## P U B L I C ###########################################
53 ##################################################################################################
62 url
: str = '127.0.0.1:17070',
63 username
: str = 'admin',
64 vca_config
: dict = None,
67 """Initialize juju N2VC connector
70 # parent class constructor
71 N2VCConnector
.__init
__(
79 vca_config
=vca_config
,
80 on_update_db
=on_update_db
83 # silence websocket traffic log
84 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
85 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
86 logging
.getLogger('model').setLevel(logging
.WARN
)
88 self
.info('Initializing N2VC juju connector...')
91 ##############################################################
93 ##############################################################
98 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
99 url_parts
= url
.split(':')
100 if len(url_parts
) != 2:
101 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url
), ['url'])
102 self
.hostname
= url_parts
[0]
104 self
.port
= int(url_parts
[1])
106 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url
), ['url'])
110 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
113 if vca_config
is None:
114 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
116 if 'secret' in vca_config
:
117 self
.secret
= vca_config
['secret']
119 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
121 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
122 # if exists, it will be written in lcm container: _create_juju_public_key()
123 if 'public_key' in vca_config
:
124 self
.public_key
= vca_config
['public_key']
126 self
.public_key
= None
128 # TODO: Verify ca_cert is valid before using. VCA will crash
129 # if the ca_cert isn't formatted correctly.
130 def base64_to_cacert(b64string
):
131 """Convert the base64-encoded string containing the VCA CACERT.
137 cacert
= base64
.b64decode(b64string
).decode("utf-8")
144 except binascii
.Error
as e
:
145 self
.debug("Caught binascii.Error: {}".format(e
))
146 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
150 self
.ca_cert
= vca_config
.get('ca_cert')
152 self
.ca_cert
= base64_to_cacert(vca_config
['ca_cert'])
154 if 'api_proxy' in vca_config
:
155 self
.api_proxy
= vca_config
['api_proxy']
156 self
.debug('api_proxy for native charms configured: {}'.format(self
.api_proxy
))
158 self
.warning('api_proxy is not configured. Support for native charms is disabled')
160 if 'enable_os_upgrade' in vca_config
:
161 self
.enable_os_upgrade
= vca_config
['enable_os_upgrade']
163 self
.enable_os_upgrade
= True
165 if 'apt_mirror' in vca_config
:
166 self
.apt_mirror
= vca_config
['apt_mirror']
168 self
.apt_mirror
= None
170 self
.debug('Arguments have been checked')
173 self
.controller
= None # it will be filled when connect to juju
174 self
.juju_models
= {} # model objects for every model_name
175 self
.juju_observers
= {} # model observers for every model_name
176 self
._connecting
= False # while connecting to juju (to avoid duplicate connections)
177 self
._authenticated
= False # it will be True when juju connection be stablished
178 self
._creating
_model
= False # True during model creation
180 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
181 self
._create
_juju
_public
_key
()
183 self
.info('N2VC juju connector initialized')
185 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
187 # self.info('Getting NS status. namespace: {}'.format(namespace))
189 if not self
._authenticated
:
190 await self
._juju
_login
()
192 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
193 # model name is ns_id
195 if model_name
is None:
196 msg
= 'Namespace {} not valid'.format(namespace
)
198 raise N2VCBadArgumentsException(msg
, ['namespace'])
200 # get juju model (create model if needed)
201 model
= await self
._juju
_get
_model
(model_name
=model_name
)
203 status
= await model
.get_status()
206 return obj_to_yaml(status
)
208 return obj_to_dict(status
)
210 async def create_execution_environment(
214 reuse_ee_id
: str = None,
215 progress_timeout
: float = None,
216 total_timeout
: float = None
219 self
.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace
, reuse_ee_id
))
221 if not self
._authenticated
:
222 await self
._juju
_login
()
226 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=reuse_ee_id
)
228 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
229 # model name is ns_id
232 application_name
= self
._get
_application
_name
(namespace
=namespace
)
234 self
.debug('model name: {}, application name: {}, machine_id: {}'
235 .format(model_name
, application_name
, machine_id
))
237 # create or reuse a new juju machine
239 machine
= await self
._juju
_create
_machine
(
240 model_name
=model_name
,
241 application_name
=application_name
,
242 machine_id
=machine_id
,
244 progress_timeout
=progress_timeout
,
245 total_timeout
=total_timeout
247 except Exception as e
:
248 message
= 'Error creating machine on juju: {}'.format(e
)
250 raise N2VCException(message
=message
)
252 # id for the execution environment
253 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
254 model_name
=model_name
,
255 application_name
=application_name
,
256 machine_id
=str(machine
.entity_id
)
258 self
.debug('ee_id: {}'.format(ee_id
))
260 # new machine credentials
262 credentials
['hostname'] = machine
.dns_name
264 self
.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id
, credentials
))
266 return ee_id
, credentials
268 async def register_execution_environment(
273 progress_timeout
: float = None,
274 total_timeout
: float = None
277 if not self
._authenticated
:
278 await self
._juju
_login
()
280 self
.info('Registering execution environment. namespace={}, credentials={}'.format(namespace
, credentials
))
282 if credentials
is None:
283 raise N2VCBadArgumentsException(message
='credentials are mandatory', bad_args
=['credentials'])
284 if credentials
.get('hostname'):
285 hostname
= credentials
['hostname']
287 raise N2VCBadArgumentsException(message
='hostname is mandatory', bad_args
=['credentials.hostname'])
288 if credentials
.get('username'):
289 username
= credentials
['username']
291 raise N2VCBadArgumentsException(message
='username is mandatory', bad_args
=['credentials.username'])
292 if 'private_key_path' in credentials
:
293 private_key_path
= credentials
['private_key_path']
295 # if not passed as argument, use generated private key path
296 private_key_path
= self
.private_key_path
298 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
303 application_name
= self
._get
_application
_name
(namespace
=namespace
)
305 # register machine on juju
307 machine_id
= await self
._juju
_provision
_machine
(
308 model_name
=model_name
,
311 private_key_path
=private_key_path
,
313 progress_timeout
=progress_timeout
,
314 total_timeout
=total_timeout
316 except Exception as e
:
317 self
.error('Error registering machine: {}'.format(e
))
318 raise N2VCException(message
='Error registering machine on juju: {}'.format(e
))
320 self
.info('Machine registered: {}'.format(machine_id
))
322 # id for the execution environment
323 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
324 model_name
=model_name
,
325 application_name
=application_name
,
326 machine_id
=str(machine_id
)
329 self
.info('Execution environment registered. ee_id: {}'.format(ee_id
))
333 async def install_configuration_sw(
338 progress_timeout
: float = None,
339 total_timeout
: float = None
342 self
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
343 .format(ee_id
, artifact_path
, db_dict
))
345 if not self
._authenticated
:
346 await self
._juju
_login
()
349 if ee_id
is None or len(ee_id
) == 0:
350 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
351 if artifact_path
is None or len(artifact_path
) == 0:
352 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
354 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
357 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
358 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
359 except Exception as e
:
360 raise N2VCBadArgumentsException(
361 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
365 # remove // in charm path
366 while artifact_path
.find('//') >= 0:
367 artifact_path
= artifact_path
.replace('//', '/')
370 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
371 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
372 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
374 if artifact_path
.startswith('/'):
375 full_path
= self
.fs
.path
+ artifact_path
377 full_path
= self
.fs
.path
+ '/' + artifact_path
380 application
, retries
= await self
._juju
_deploy
_charm
(
381 model_name
=model_name
,
382 application_name
=application_name
,
383 charm_path
=full_path
,
384 machine_id
=machine_id
,
386 progress_timeout
=progress_timeout
,
387 total_timeout
=total_timeout
389 except Exception as e
:
390 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
392 self
.info('Configuration sw installed')
394 async def get_ee_ssh_public__key(
398 progress_timeout
: float = None,
399 total_timeout
: float = None
402 self
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
404 if not self
._authenticated
:
405 await self
._juju
_login
()
408 if ee_id
is None or len(ee_id
) == 0:
409 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
411 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
414 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
415 self
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
416 except Exception as e
:
417 raise N2VCBadArgumentsException(
418 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
422 # try to execute ssh layer primitives (if exist):
428 # execute action: generate-ssh-key
430 output
, status
= await self
._juju
_execute
_action
(
431 model_name
=model_name
,
432 application_name
=application_name
,
433 action_name
='generate-ssh-key',
435 progress_timeout
=progress_timeout
,
436 total_timeout
=total_timeout
438 except Exception as e
:
439 self
.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e
))
441 # execute action: get-ssh-public-key
443 output
, status
= await self
._juju
_execute
_action
(
444 model_name
=model_name
,
445 application_name
=application_name
,
446 action_name
='get-ssh-public-key',
448 progress_timeout
=progress_timeout
,
449 total_timeout
=total_timeout
451 except Exception as e
:
452 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
456 # return public key if exists
457 return output
["pubkey"] if "pubkey" in output
else output
459 async def add_relation(
467 self
.debug('adding new relation between {} and {}, endpoints: {}, {}'
468 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
470 if not self
._authenticated
:
471 await self
._juju
_login
()
473 # get model, application and machines
474 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
475 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
477 # model must be the same
478 if model_1
!= model_2
:
479 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
481 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
483 # add juju relations between two applications
485 self
._juju
_add
_relation
()
486 except Exception as e
:
487 message
= 'Error adding relation between {} and {}'.format(ee_id_1
, ee_id_2
)
489 raise N2VCException(message
=message
)
491 async def remove_relation(
494 if not self
._authenticated
:
495 await self
._juju
_login
()
497 self
.info('Method not implemented yet')
498 raise NotImplemented()
500 async def deregister_execution_environments(
503 if not self
._authenticated
:
504 await self
._juju
_login
()
506 self
.info('Method not implemented yet')
507 raise NotImplemented()
509 async def delete_namespace(
512 db_dict
: dict = None,
513 total_timeout
: float = None
515 self
.info('Deleting namespace={}'.format(namespace
))
517 if not self
._authenticated
:
518 await self
._juju
_login
()
521 if namespace
is None:
522 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
524 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
525 if ns_id
is not None:
527 await self
._juju
_destroy
_model
(
529 total_timeout
=total_timeout
531 except Exception as e
:
532 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
534 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
536 self
.info('Namespace {} deleted'.format(namespace
))
538 async def delete_execution_environment(
541 db_dict
: dict = None,
542 total_timeout
: float = None
544 self
.info('Deleting execution environment ee_id={}'.format(ee_id
))
546 if not self
._authenticated
:
547 await self
._juju
_login
()
551 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
553 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
555 # destroy the application
557 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
558 except Exception as e
:
559 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
560 .format(ee_id
, application_name
, e
))
562 # destroy the machine
564 await self
._juju
_destroy
_machine
(
565 model_name
=model_name
,
566 machine_id
=machine_id
,
567 total_timeout
=total_timeout
569 except Exception as e
:
570 raise N2VCException(message
='Error deleting execution environment {} (machine {}) : {}'
571 .format(ee_id
, machine_id
, e
))
573 self
.info('Execution environment {} deleted'.format(ee_id
))
575 async def exec_primitive(
580 db_dict
: dict = None,
581 progress_timeout
: float = None,
582 total_timeout
: float = None
585 self
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
587 if not self
._authenticated
:
588 await self
._juju
_login
()
591 if ee_id
is None or len(ee_id
) == 0:
592 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
593 if primitive_name
is None or len(primitive_name
) == 0:
594 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
595 if params_dict
is None:
599 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
601 raise N2VCBadArgumentsException(
602 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
606 if primitive_name
== 'config':
607 # Special case: config primitive
609 await self
._juju
_configure
_application
(
610 model_name
=model_name
,
611 application_name
=application_name
,
614 progress_timeout
=progress_timeout
,
615 total_timeout
=total_timeout
617 except Exception as e
:
618 self
.error('Error configuring juju application: {}'.format(e
))
619 raise N2VCExecutionException(
620 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
621 primitive_name
=primitive_name
626 output
, status
= await self
._juju
_execute
_action
(
627 model_name
=model_name
,
628 application_name
=application_name
,
629 action_name
=primitive_name
,
631 progress_timeout
=progress_timeout
,
632 total_timeout
=total_timeout
,
635 if status
== 'completed':
638 raise Exception('status is not completed: {}'.format(status
))
639 except Exception as e
:
640 self
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
641 raise N2VCExecutionException(
642 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
643 primitive_name
=primitive_name
646 async def disconnect(self
):
647 self
.info('closing juju N2VC...')
648 await self
._juju
_logout
()
651 ##################################################################################################
652 ########################################## P R I V A T E #########################################
653 ##################################################################################################
662 # write ee_id to database: _admin.deployed.VCA.x
664 the_table
= db_dict
['collection']
665 the_filter
= db_dict
['filter']
666 the_path
= db_dict
['path']
667 if not the_path
[-1] == '.':
668 the_path
= the_path
+ '.'
669 update_dict
= {the_path
+ 'ee_id': ee_id
}
670 # self.debug('Writing ee_id to database: {}'.format(the_path))
674 update_dict
=update_dict
,
677 except Exception as e
:
678 self
.error('Error writing ee_id to database: {}'.format(e
))
683 application_name
: str,
687 Build an execution environment id form model, application and machine
689 :param application_name:
693 # id for the execution environment
694 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
697 def _get_ee_id_components(
699 ) -> (str, str, str):
701 Get model, application and machine components from an execution environment id
703 :return: model_name, application_name, machine_id
707 return None, None, None
709 # split components of id
710 parts
= ee_id
.split('.')
711 model_name
= parts
[0]
712 application_name
= parts
[1]
713 machine_id
= parts
[2]
714 return model_name
, application_name
, machine_id
716 def _get_application_name(self
, namespace
: str) -> str:
718 Build application name from namespace
720 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
723 # TODO: Enforce the Juju 50-character application limit
725 # split namespace components
726 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
728 if vnf_id
is None or len(vnf_id
) == 0:
731 # Shorten the vnf_id to its last twelve characters
732 vnf_id
= 'vnf-' + vnf_id
[-12:]
734 if vdu_id
is None or len(vdu_id
) == 0:
737 # Shorten the vdu_id to its last twelve characters
738 vdu_id
= '-vdu-' + vdu_id
[-12:]
740 if vdu_count
is None or len(vdu_count
) == 0:
743 vdu_count
= '-cnt-' + vdu_count
745 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
747 return N2VCJujuConnector
._format
_app
_name
(application_name
)
749 async def _juju_create_machine(
752 application_name
: str,
753 machine_id
: str = None,
754 db_dict
: dict = None,
755 progress_timeout
: float = None,
756 total_timeout
: float = None
759 self
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
761 # get juju model and observer (create model if needed)
762 model
= await self
._juju
_get
_model
(model_name
=model_name
)
763 observer
= self
.juju_observers
[model_name
]
765 # find machine id in model
767 if machine_id
is not None:
768 self
.debug('Finding existing machine id {} in model'.format(machine_id
))
769 # get juju existing machines in the model
770 existing_machines
= await model
.get_machines()
771 if machine_id
in existing_machines
:
772 self
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
773 machine
= model
.machines
[machine_id
]
776 self
.debug('Creating a new machine in juju...')
777 # machine does not exist, create it and wait for it
778 machine
= await model
.add_machine(
785 # register machine with observer
786 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
788 # id for the execution environment
789 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
790 model_name
=model_name
,
791 application_name
=application_name
,
792 machine_id
=str(machine
.entity_id
)
795 # write ee_id in database
796 self
._write
_ee
_id
_db
(
801 # wait for machine creation
802 await observer
.wait_for_machine(
803 machine_id
=str(machine
.entity_id
),
804 progress_timeout
=progress_timeout
,
805 total_timeout
=total_timeout
810 self
.debug('Reusing old machine pending')
812 # register machine with observer
813 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
815 # machine does exist, but it is in creation process (pending), wait for create finalisation
816 await observer
.wait_for_machine(
817 machine_id
=machine
.entity_id
,
818 progress_timeout
=progress_timeout
,
819 total_timeout
=total_timeout
)
821 self
.debug("Machine ready at " + str(machine
.dns_name
))
824 async def _juju_provision_machine(
829 private_key_path
: str,
830 db_dict
: dict = None,
831 progress_timeout
: float = None,
832 total_timeout
: float = None
835 if not self
.api_proxy
:
836 msg
= 'Cannot provision machine: api_proxy is not defined'
838 raise N2VCException(message
=msg
)
840 self
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
842 if not self
._authenticated
:
843 await self
._juju
_login
()
845 # get juju model and observer
846 model
= await self
._juju
_get
_model
(model_name
=model_name
)
847 observer
= self
.juju_observers
[model_name
]
849 # TODO check if machine is already provisioned
850 machine_list
= await model
.get_machines()
852 provisioner
= SSHProvisioner(
855 private_key_path
=private_key_path
,
861 params
= provisioner
.provision_machine()
862 except Exception as ex
:
863 msg
= "Exception provisioning machine: {}".format(ex
)
865 raise N2VCException(message
=msg
)
867 params
.jobs
= ['JobHostUnits']
869 connection
= model
.connection()
871 # Submit the request.
872 self
.debug("Adding machine to model")
873 client_facade
= client
.ClientFacade
.from_connection(connection
)
874 results
= await client_facade
.AddMachines(params
=[params
])
875 error
= results
.machines
[0].error
877 msg
= "Error adding machine: {}}".format(error
.message
)
879 raise ValueError(msg
)
881 machine_id
= results
.machines
[0].machine
883 # Need to run this after AddMachines has been called,
884 # as we need the machine_id
885 self
.debug("Installing Juju agent into machine {}".format(machine_id
))
886 asyncio
.ensure_future(provisioner
.install_agent(
887 connection
=connection
,
889 machine_id
=machine_id
,
893 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
896 machine_list
= await model
.get_machines()
897 if machine_id
in machine_list
:
898 self
.debug('Machine {} found in model!'.format(machine_id
))
899 machine
= model
.machines
.get(machine_id
)
901 await asyncio
.sleep(2)
904 msg
= 'Machine {} not found in model'.format(machine_id
)
908 # register machine with observer
909 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
911 # wait for machine creation
912 self
.debug('waiting for provision finishes... {}'.format(machine_id
))
913 await observer
.wait_for_machine(
914 machine_id
=machine_id
,
915 progress_timeout
=progress_timeout
,
916 total_timeout
=total_timeout
919 self
.debug("Machine provisioned {}".format(machine_id
))
923 async def _juju_deploy_charm(
926 application_name
: str,
930 progress_timeout
: float = None,
931 total_timeout
: float = None
932 ) -> (Application
, int):
934 # get juju model and observer
935 model
= await self
._juju
_get
_model
(model_name
=model_name
)
936 observer
= self
.juju_observers
[model_name
]
938 # check if application already exists
940 if application_name
in model
.applications
:
941 application
= model
.applications
[application_name
]
943 if application
is None:
945 # application does not exist, create it and wait for it
946 self
.debug('deploying application {} to machine {}, model {}'
947 .format(application_name
, machine_id
, model_name
))
948 self
.debug('charm: {}'.format(charm_path
))
951 application
= await model
.deploy(
952 entity_url
=charm_path
,
953 application_name
=application_name
,
960 # register application with observer
961 observer
.register_application(application
=application
, db_dict
=db_dict
)
963 self
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
964 retries
= await observer
.wait_for_application(
965 application_id
=application
.entity_id
,
966 progress_timeout
=progress_timeout
,
967 total_timeout
=total_timeout
)
968 self
.debug('application deployed')
972 # register application with observer
973 observer
.register_application(application
=application
, db_dict
=db_dict
)
975 # application already exists, but not finalised
976 self
.debug('application already exists, waiting for deployed...')
977 retries
= await observer
.wait_for_application(
978 application_id
=application
.entity_id
,
979 progress_timeout
=progress_timeout
,
980 total_timeout
=total_timeout
)
981 self
.debug('application deployed')
983 return application
, retries
985 async def _juju_execute_action(
988 application_name
: str,
991 progress_timeout
: float = None,
992 total_timeout
: float = None,
996 # get juju model and observer
997 model
= await self
._juju
_get
_model
(model_name
=model_name
)
998 observer
= self
.juju_observers
[model_name
]
1000 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1002 unit
= application
.units
[0]
1003 if unit
is not None:
1004 actions
= await application
.get_actions()
1005 if action_name
in actions
:
1006 self
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1007 action
= await unit
.run_action(action_name
, **kwargs
)
1009 # register action with observer
1010 observer
.register_action(action
=action
, db_dict
=db_dict
)
1012 await observer
.wait_for_action(
1013 action_id
=action
.entity_id
,
1014 progress_timeout
=progress_timeout
,
1015 total_timeout
=total_timeout
)
1016 self
.debug('action completed with status: {}'.format(action
.status
))
1017 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1018 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1019 if action
.entity_id
in status
:
1020 status
= status
[action
.entity_id
]
1023 return output
, status
1025 raise N2VCExecutionException(
1026 message
='Cannot execute action on charm',
1027 primitive_name
=action_name
1030 async def _juju_configure_application(
1033 application_name
: str,
1036 progress_timeout
: float = None,
1037 total_timeout
: float = None
1040 # get the application
1041 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1043 self
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1044 res
= await application
.set_config(config
)
1045 self
.debug('application {} configured. res={}'.format(application_name
, res
))
1047 # Verify the config is set
1048 new_conf
= await application
.get_config()
1050 value
= new_conf
[key
]['value']
1051 self
.debug(' {} = {}'.format(key
, value
))
1052 if config
[key
] != value
:
1053 raise N2VCException(
1054 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1057 # check if 'verify-ssh-credentials' action exists
1058 # unit = application.units[0]
1059 actions
= await application
.get_actions()
1060 if 'verify-ssh-credentials' not in actions
:
1061 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1065 # execute verify-credentials
1067 retry_timeout
= 15.0
1068 for i
in range(num_retries
):
1070 self
.debug('Executing action verify-ssh-credentials...')
1071 output
, ok
= await self
._juju
_execute
_action
(
1072 model_name
=model_name
,
1073 application_name
=application_name
,
1074 action_name
='verify-ssh-credentials',
1076 progress_timeout
=progress_timeout
,
1077 total_timeout
=total_timeout
1079 self
.debug('Result: {}, output: {}'.format(ok
, output
))
1081 except Exception as e
:
1082 self
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1083 await asyncio
.sleep(retry_timeout
)
1085 self
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1088 async def _juju_get_application(
1091 application_name
: str
1093 """Get the deployed application."""
1095 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1097 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1099 if model
.applications
and application_name
in model
.applications
:
1100 return model
.applications
[application_name
]
1102 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1104 async def _juju_get_model(self
, model_name
: str) -> Model
:
1105 """ Get a model object from juju controller
1106 If the model does not exits, it creates it.
1108 :param str model_name: name of the model
1109 :returns Model: model obtained from juju controller or Exception
1113 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1115 if model_name
in self
.juju_models
:
1116 return self
.juju_models
[model_name
]
1118 if self
._creating
_model
:
1119 self
.debug('Another coroutine is creating a model. Wait...')
1120 while self
._creating
_model
:
1121 # another coroutine is creating a model, wait
1122 await asyncio
.sleep(0.1)
1123 # retry (perhaps another coroutine has created the model meanwhile)
1124 if model_name
in self
.juju_models
:
1125 return self
.juju_models
[model_name
]
1128 self
._creating
_model
= True
1130 # get juju model names from juju
1131 model_list
= await self
.controller
.list_models()
1133 if model_name
not in model_list
:
1134 self
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1135 config_dict
= {'authorized-keys': self
.public_key
}
1137 config_dict
['apt-mirror'] = self
.apt_mirror
1138 if not self
.enable_os_upgrade
:
1139 config_dict
['enable-os-refresh-update'] = False
1140 config_dict
['enable-os-upgrade'] = False
1142 model
= await self
.controller
.add_model(
1143 model_name
=model_name
,
1146 self
.info('New model created, name={}'.format(model_name
))
1148 self
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1149 model
= await self
.controller
.get_model(model_name
)
1150 self
.debug('Existing model in juju, name={}'.format(model_name
))
1152 self
.juju_models
[model_name
] = model
1153 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1156 except Exception as e
:
1157 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1159 raise N2VCException(msg
)
1161 self
._creating
_model
= False
1163 async def _juju_add_relation(
1166 application_name_1
: str,
1167 application_name_2
: str,
1172 self
.debug('adding relation')
1174 # get juju model and observer
1175 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1177 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1178 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1179 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1181 async def _juju_destroy_application(
1184 application_name
: str
1187 self
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1189 # get juju model and observer
1190 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1192 application
= model
.applications
.get(application_name
)
1194 await application
.destroy()
1196 self
.debug('Application not found: {}'.format(application_name
))
1198 async def _juju_destroy_machine(
1202 total_timeout
: float = None
1205 self
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1207 if total_timeout
is None:
1208 total_timeout
= 3600
1210 # get juju model and observer
1211 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1213 machines
= await model
.get_machines()
1214 if machine_id
in machines
:
1215 machine
= model
.machines
[machine_id
]
1216 await machine
.destroy(force
=True)
1218 end
= time
.time() + total_timeout
1219 # wait for machine removal
1220 machines
= await model
.get_machines()
1221 while machine_id
in machines
and time
.time() < end
:
1222 self
.debug('Waiting for machine {} is destroyed'.format(machine_id
))
1223 await asyncio
.sleep(0.5)
1224 machines
= await model
.get_machines()
1225 self
.debug('Machine destroyed: {}'.format(machine_id
))
1227 self
.debug('Machine not found: {}'.format(machine_id
))
1229 async def _juju_destroy_model(
1232 total_timeout
: float = None
1235 self
.debug('Destroying model {}'.format(model_name
))
1237 if total_timeout
is None:
1238 total_timeout
= 3600
1240 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1241 uuid
= model
.info
.uuid
1243 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1244 self
.juju_models
[model_name
] = None
1245 self
.juju_observers
[model_name
] = None
1247 self
.debug('destroying model {}...'.format(model_name
))
1248 await self
.controller
.destroy_model(uuid
)
1249 self
.debug('model destroy requested {}'.format(model_name
))
1251 # wait for model is completely destroyed
1252 end
= time
.time() + total_timeout
1253 while time
.time() < end
:
1254 self
.debug('Waiting for model is destroyed...')
1256 # await self.controller.get_model(uuid)
1257 models
= await self
.controller
.list_models()
1258 if model_name
not in models
:
1259 self
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1261 except Exception as e
:
1263 await asyncio
.sleep(1.0)
1265 async def _juju_login(self
):
1266 """Connect to juju controller
1270 # if already authenticated, exit function
1271 if self
._authenticated
:
1274 # if connecting, wait for finish
1275 # another task could be trying to connect in parallel
1276 while self
._connecting
:
1277 await asyncio
.sleep(0.1)
1279 # double check after other task has finished
1280 if self
._authenticated
:
1284 self
._connecting
= True
1286 'connecting to juju controller: {} {}:{} ca_cert: {}'
1287 .format(self
.url
, self
.username
, self
.secret
, '\n'+self
.ca_cert
if self
.ca_cert
else 'None'))
1289 # Create controller object
1290 self
.controller
= Controller(loop
=self
.loop
)
1291 # Connect to controller
1292 await self
.controller
.connect(
1294 username
=self
.username
,
1295 password
=self
.secret
,
1298 self
._authenticated
= True
1299 self
.info('juju controller connected')
1300 except Exception as e
:
1301 message
= 'Exception connecting to juju: {}'.format(e
)
1303 raise N2VCConnectionException(
1308 self
._connecting
= False
1310 async def _juju_logout(self
):
1311 """Logout of the Juju controller."""
1312 if not self
._authenticated
:
1315 # disconnect all models
1316 for model_name
in self
.juju_models
:
1318 await self
._juju
_disconnect
_model
(model_name
)
1319 except Exception as e
:
1320 self
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1321 # continue with next model...
1323 self
.info("Disconnecting controller")
1325 await self
.controller
.disconnect()
1326 except Exception as e
:
1327 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1329 self
.controller
= None
1330 self
._authenticated
= False
1331 self
.info('disconnected')
1333 async def _juju_disconnect_model(
1337 self
.debug("Disconnecting model {}".format(model_name
))
1338 if model_name
in self
.juju_models
:
1339 await self
.juju_models
[model_name
].disconnect()
1340 self
.juju_models
[model_name
] = None
1341 self
.juju_observers
[model_name
] = None
1343 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1345 def _create_juju_public_key(self
):
1346 """Recreate the Juju public key on lcm container, if needed
1347 Certain libjuju commands expect to be run from the same machine as Juju
1348 is bootstrapped to. This method will write the public key to disk in
1349 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1352 # Make sure that we have a public key before writing to disk
1353 if self
.public_key
is None or len(self
.public_key
) == 0:
1354 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1355 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1356 if len(self
.public_key
) == 0:
1361 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1362 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1363 self
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1364 if not os
.path
.exists(pk_path
):
1365 # create path and write file
1366 os
.makedirs(pk_path
)
1367 with
open(file_path
, 'w') as f
:
1368 self
.debug('Creating juju public key file: {}'.format(file_path
))
1369 f
.write(self
.public_key
)
1371 self
.debug('juju public key file already exists: {}'.format(file_path
))
1374 def _format_model_name(name
: str) -> str:
1375 """Format the name of the model.
1377 Model names may only contain lowercase letters, digits and hyphens
1380 return name
.replace('_', '-').replace(' ', '-').lower()
1383 def _format_app_name(name
: str) -> str:
1384 """Format the name of the application (in order to assure valid application name).
1386 Application names have restrictions (run juju deploy --help):
1387 - contains lowercase letters 'a'-'z'
1388 - contains numbers '0'-'9'
1389 - contains hyphens '-'
1390 - starts with a lowercase letter
1391 - not two or more consecutive hyphens
1392 - after a hyphen, not a group with all numbers
1395 def all_numbers(s
: str) -> bool:
1401 new_name
= name
.replace('_', '-')
1402 new_name
= new_name
.replace(' ', '-')
1403 new_name
= new_name
.lower()
1404 while new_name
.find('--') >= 0:
1405 new_name
= new_name
.replace('--', '-')
1406 groups
= new_name
.split('-')
1408 # find 'all numbers' groups and prefix them with a letter
1410 for i
in range(len(groups
)):
1412 if all_numbers(group
):
1418 if app_name
[0].isdigit():
1419 app_name
= 'z' + app_name