2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
31 from n2vc
.n2vc_conn
import N2VCConnector
32 from n2vc
.n2vc_conn
import obj_to_dict
, obj_to_yaml
33 from n2vc
.exceptions \
34 import N2VCBadArgumentsException
, N2VCException
, N2VCConnectionException
, \
35 N2VCExecutionException
, N2VCInvalidCertificate
, N2VCNotFound
36 from n2vc
.juju_observer
import JujuModelObserver
38 from juju
.controller
import Controller
39 from juju
.model
import Model
40 from juju
.application
import Application
41 from juju
.action
import Action
42 from juju
.machine
import Machine
43 from juju
.client
import client
44 from juju
.errors
import JujuAPIError
46 from n2vc
.provisioner
import SSHProvisioner
49 class N2VCJujuConnector(N2VCConnector
):
52 ##################################################################################################
53 ########################################## P U B L I C ###########################################
54 ##################################################################################################
57 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
65 url
: str = '127.0.0.1:17070',
66 username
: str = 'admin',
67 vca_config
: dict = None,
70 """Initialize juju N2VC connector
73 # parent class constructor
74 N2VCConnector
.__init
__(
82 vca_config
=vca_config
,
83 on_update_db
=on_update_db
86 # silence websocket traffic log
87 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
88 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
89 logging
.getLogger('model').setLevel(logging
.WARN
)
91 self
.log
.info('Initializing N2VC juju connector...')
94 ##############################################################
96 ##############################################################
101 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
102 url_parts
= url
.split(':')
103 if len(url_parts
) != 2:
104 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url
), ['url'])
105 self
.hostname
= url_parts
[0]
107 self
.port
= int(url_parts
[1])
109 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url
), ['url'])
113 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
116 if vca_config
is None:
117 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
119 if 'secret' in vca_config
:
120 self
.secret
= vca_config
['secret']
122 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
124 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
125 # if exists, it will be written in lcm container: _create_juju_public_key()
126 if 'public_key' in vca_config
:
127 self
.public_key
= vca_config
['public_key']
129 self
.public_key
= None
131 # TODO: Verify ca_cert is valid before using. VCA will crash
132 # if the ca_cert isn't formatted correctly.
133 def base64_to_cacert(b64string
):
134 """Convert the base64-encoded string containing the VCA CACERT.
140 cacert
= base64
.b64decode(b64string
).decode("utf-8")
147 except binascii
.Error
as e
:
148 self
.log
.debug("Caught binascii.Error: {}".format(e
))
149 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
153 self
.ca_cert
= vca_config
.get('ca_cert')
155 self
.ca_cert
= base64_to_cacert(vca_config
['ca_cert'])
157 if 'api_proxy' in vca_config
:
158 self
.api_proxy
= vca_config
['api_proxy']
159 self
.log
.debug('api_proxy for native charms configured: {}'.format(self
.api_proxy
))
161 self
.warning('api_proxy is not configured. Support for native charms is disabled')
163 if 'enable_os_upgrade' in vca_config
:
164 self
.enable_os_upgrade
= vca_config
['enable_os_upgrade']
166 self
.enable_os_upgrade
= True
168 if 'apt_mirror' in vca_config
:
169 self
.apt_mirror
= vca_config
['apt_mirror']
171 self
.apt_mirror
= None
173 self
.cloud
= vca_config
.get('cloud')
174 # self.log.debug('Arguments have been checked')
177 self
.controller
= None # it will be filled when connect to juju
178 self
.juju_models
= {} # model objects for every model_name
179 self
.juju_observers
= {} # model observers for every model_name
180 self
._connecting
= False # while connecting to juju (to avoid duplicate connections)
181 self
._authenticated
= False # it will be True when juju connection be stablished
182 self
._creating
_model
= False # True during model creation
184 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
185 self
._create
_juju
_public
_key
()
187 self
.log
.info('N2VC juju connector initialized')
189 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
191 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
193 if not self
._authenticated
:
194 await self
._juju
_login
()
196 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
197 # model name is ns_id
199 if model_name
is None:
200 msg
= 'Namespace {} not valid'.format(namespace
)
202 raise N2VCBadArgumentsException(msg
, ['namespace'])
204 # get juju model (create model if needed)
205 model
= await self
._juju
_get
_model
(model_name
=model_name
)
207 status
= await model
.get_status()
210 return obj_to_yaml(status
)
212 return obj_to_dict(status
)
214 async def create_execution_environment(
218 reuse_ee_id
: str = None,
219 progress_timeout
: float = None,
220 total_timeout
: float = None
223 self
.log
.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace
, reuse_ee_id
))
225 if not self
._authenticated
:
226 await self
._juju
_login
()
230 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=reuse_ee_id
)
232 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
233 # model name is ns_id
236 application_name
= self
._get
_application
_name
(namespace
=namespace
)
238 self
.log
.debug('model name: {}, application name: {}, machine_id: {}'
239 .format(model_name
, application_name
, machine_id
))
241 # create or reuse a new juju machine
243 machine
= await self
._juju
_create
_machine
(
244 model_name
=model_name
,
245 application_name
=application_name
,
246 machine_id
=machine_id
,
248 progress_timeout
=progress_timeout
,
249 total_timeout
=total_timeout
251 except Exception as e
:
252 message
= 'Error creating machine on juju: {}'.format(e
)
253 self
.log
.error(message
)
254 raise N2VCException(message
=message
)
256 # id for the execution environment
257 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
258 model_name
=model_name
,
259 application_name
=application_name
,
260 machine_id
=str(machine
.entity_id
)
262 self
.log
.debug('ee_id: {}'.format(ee_id
))
264 # new machine credentials
266 credentials
['hostname'] = machine
.dns_name
268 self
.log
.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id
, credentials
))
270 return ee_id
, credentials
272 async def register_execution_environment(
277 progress_timeout
: float = None,
278 total_timeout
: float = None
281 if not self
._authenticated
:
282 await self
._juju
_login
()
284 self
.log
.info('Registering execution environment. namespace={}, credentials={}'.format(namespace
, credentials
))
286 if credentials
is None:
287 raise N2VCBadArgumentsException(message
='credentials are mandatory', bad_args
=['credentials'])
288 if credentials
.get('hostname'):
289 hostname
= credentials
['hostname']
291 raise N2VCBadArgumentsException(message
='hostname is mandatory', bad_args
=['credentials.hostname'])
292 if credentials
.get('username'):
293 username
= credentials
['username']
295 raise N2VCBadArgumentsException(message
='username is mandatory', bad_args
=['credentials.username'])
296 if 'private_key_path' in credentials
:
297 private_key_path
= credentials
['private_key_path']
299 # if not passed as argument, use generated private key path
300 private_key_path
= self
.private_key_path
302 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
307 application_name
= self
._get
_application
_name
(namespace
=namespace
)
309 # register machine on juju
311 machine_id
= await self
._juju
_provision
_machine
(
312 model_name
=model_name
,
315 private_key_path
=private_key_path
,
317 progress_timeout
=progress_timeout
,
318 total_timeout
=total_timeout
320 except Exception as e
:
321 self
.log
.error('Error registering machine: {}'.format(e
))
322 raise N2VCException(message
='Error registering machine on juju: {}'.format(e
))
324 self
.log
.info('Machine registered: {}'.format(machine_id
))
326 # id for the execution environment
327 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
328 model_name
=model_name
,
329 application_name
=application_name
,
330 machine_id
=str(machine_id
)
333 self
.log
.info('Execution environment registered. ee_id: {}'.format(ee_id
))
337 async def install_configuration_sw(
342 progress_timeout
: float = None,
343 total_timeout
: float = None,
347 self
.log
.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
348 .format(ee_id
, artifact_path
, db_dict
))
350 if not self
._authenticated
:
351 await self
._juju
_login
()
354 if ee_id
is None or len(ee_id
) == 0:
355 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
356 if artifact_path
is None or len(artifact_path
) == 0:
357 raise N2VCBadArgumentsException(message
='artifact_path is mandatory', bad_args
=['artifact_path'])
359 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
362 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
363 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
364 except Exception as e
:
365 raise N2VCBadArgumentsException(
366 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
370 # remove // in charm path
371 while artifact_path
.find('//') >= 0:
372 artifact_path
= artifact_path
.replace('//', '/')
375 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
376 msg
= 'artifact path does not exist: {}'.format(artifact_path
)
377 raise N2VCBadArgumentsException(message
=msg
, bad_args
=['artifact_path'])
379 if artifact_path
.startswith('/'):
380 full_path
= self
.fs
.path
+ artifact_path
382 full_path
= self
.fs
.path
+ '/' + artifact_path
385 application
, retries
= await self
._juju
_deploy
_charm
(
386 model_name
=model_name
,
387 application_name
=application_name
,
388 charm_path
=full_path
,
389 machine_id
=machine_id
,
391 progress_timeout
=progress_timeout
,
392 total_timeout
=total_timeout
,
395 except Exception as e
:
396 raise N2VCException(message
='Error desploying charm into ee={} : {}'.format(ee_id
, e
))
398 self
.log
.info('Configuration sw installed')
400 async def get_ee_ssh_public__key(
404 progress_timeout
: float = None,
405 total_timeout
: float = None
408 self
.log
.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id
, db_dict
))
410 if not self
._authenticated
:
411 await self
._juju
_login
()
414 if ee_id
is None or len(ee_id
) == 0:
415 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
417 raise N2VCBadArgumentsException(message
='db_dict is mandatory', bad_args
=['db_dict'])
420 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
421 self
.log
.debug('model: {}, application: {}, machine: {}'.format(model_name
, application_name
, machine_id
))
422 except Exception as e
:
423 raise N2VCBadArgumentsException(
424 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
428 # try to execute ssh layer primitives (if exist):
434 # execute action: generate-ssh-key
436 output
, status
= await self
._juju
_execute
_action
(
437 model_name
=model_name
,
438 application_name
=application_name
,
439 action_name
='generate-ssh-key',
441 progress_timeout
=progress_timeout
,
442 total_timeout
=total_timeout
444 except Exception as e
:
445 self
.log
.info('Skipping exception while executing action generate-ssh-key: {}'.format(e
))
447 # execute action: get-ssh-public-key
449 output
, status
= await self
._juju
_execute
_action
(
450 model_name
=model_name
,
451 application_name
=application_name
,
452 action_name
='get-ssh-public-key',
454 progress_timeout
=progress_timeout
,
455 total_timeout
=total_timeout
457 except Exception as e
:
458 msg
= 'Cannot execute action get-ssh-public-key: {}\n'.format(e
)
460 raise N2VCException(msg
)
462 # return public key if exists
463 return output
["pubkey"] if "pubkey" in output
else output
465 async def add_relation(
473 self
.log
.debug('adding new relation between {} and {}, endpoints: {}, {}'
474 .format(ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
))
478 message
= 'EE 1 is mandatory'
479 self
.log
.error(message
)
480 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1'])
482 message
= 'EE 2 is mandatory'
483 self
.log
.error(message
)
484 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_2'])
486 message
= 'endpoint 1 is mandatory'
487 self
.log
.error(message
)
488 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_1'])
490 message
= 'endpoint 2 is mandatory'
491 self
.log
.error(message
)
492 raise N2VCBadArgumentsException(message
=message
, bad_args
=['endpoint_2'])
494 if not self
._authenticated
:
495 await self
._juju
_login
()
497 # get the model, the applications and the machines from the ee_id's
498 model_1
, app_1
, machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
499 model_2
, app_2
, machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
501 # model must be the same
502 if model_1
!= model_2
:
503 message
= 'EE models are not the same: {} vs {}'.format(ee_id_1
, ee_id_2
)
504 self
.log
.error(message
)
505 raise N2VCBadArgumentsException(message
=message
, bad_args
=['ee_id_1', 'ee_id_2'])
507 # add juju relations between two applications
509 await self
._juju
_add
_relation
(
511 application_name_1
=app_1
,
512 application_name_2
=app_2
,
513 relation_1
=endpoint_1
,
514 relation_2
=endpoint_2
516 except Exception as e
:
517 message
= 'Error adding relation between {} and {}: {}'.format(ee_id_1
, ee_id_2
, e
)
518 self
.log
.error(message
)
519 raise N2VCException(message
=message
)
521 async def remove_relation(
524 if not self
._authenticated
:
525 await self
._juju
_login
()
527 self
.log
.info('Method not implemented yet')
528 raise NotImplemented()
530 async def deregister_execution_environments(
533 if not self
._authenticated
:
534 await self
._juju
_login
()
536 self
.log
.info('Method not implemented yet')
537 raise NotImplemented()
539 async def delete_namespace(
542 db_dict
: dict = None,
543 total_timeout
: float = None
545 self
.log
.info('Deleting namespace={}'.format(namespace
))
547 if not self
._authenticated
:
548 await self
._juju
_login
()
551 if namespace
is None:
552 raise N2VCBadArgumentsException(message
='namespace is mandatory', bad_args
=['namespace'])
554 nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
555 if ns_id
is not None:
557 await self
._juju
_destroy
_model
(
559 total_timeout
=total_timeout
563 except Exception as e
:
564 raise N2VCException(message
='Error deleting namespace {} : {}'.format(namespace
, e
))
566 raise N2VCBadArgumentsException(message
='only ns_id is permitted to delete yet', bad_args
=['namespace'])
568 self
.log
.info('Namespace {} deleted'.format(namespace
))
570 async def delete_execution_environment(
573 db_dict
: dict = None,
574 total_timeout
: float = None
576 self
.log
.info('Deleting execution environment ee_id={}'.format(ee_id
))
578 if not self
._authenticated
:
579 await self
._juju
_login
()
583 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
585 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(ee_id
=ee_id
)
587 # destroy the application
589 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
590 except Exception as e
:
591 raise N2VCException(message
='Error deleting execution environment {} (application {}) : {}'
592 .format(ee_id
, application_name
, e
))
594 # destroy the machine
596 # await self._juju_destroy_machine(
597 # model_name=model_name,
598 # machine_id=machine_id,
599 # total_timeout=total_timeout
601 # except Exception as e:
602 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
603 # .format(ee_id, machine_id, e))
605 self
.log
.info('Execution environment {} deleted'.format(ee_id
))
607 async def exec_primitive(
612 db_dict
: dict = None,
613 progress_timeout
: float = None,
614 total_timeout
: float = None
617 self
.log
.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name
, ee_id
, params_dict
))
619 if not self
._authenticated
:
620 await self
._juju
_login
()
623 if ee_id
is None or len(ee_id
) == 0:
624 raise N2VCBadArgumentsException(message
='ee_id is mandatory', bad_args
=['ee_id'])
625 if primitive_name
is None or len(primitive_name
) == 0:
626 raise N2VCBadArgumentsException(message
='action_name is mandatory', bad_args
=['action_name'])
627 if params_dict
is None:
631 model_name
, application_name
, machine_id
= N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
633 raise N2VCBadArgumentsException(
634 message
='ee_id={} is not a valid execution environment id'.format(ee_id
),
638 if primitive_name
== 'config':
639 # Special case: config primitive
641 await self
._juju
_configure
_application
(
642 model_name
=model_name
,
643 application_name
=application_name
,
646 progress_timeout
=progress_timeout
,
647 total_timeout
=total_timeout
649 except Exception as e
:
650 self
.log
.error('Error configuring juju application: {}'.format(e
))
651 raise N2VCExecutionException(
652 message
='Error configuring application into ee={} : {}'.format(ee_id
, e
),
653 primitive_name
=primitive_name
658 output
, status
= await self
._juju
_execute
_action
(
659 model_name
=model_name
,
660 application_name
=application_name
,
661 action_name
=primitive_name
,
663 progress_timeout
=progress_timeout
,
664 total_timeout
=total_timeout
,
667 if status
== 'completed':
670 raise Exception('status is not completed: {}'.format(status
))
671 except Exception as e
:
672 self
.log
.error('Error executing primitive {}: {}'.format(primitive_name
, e
))
673 raise N2VCExecutionException(
674 message
='Error executing primitive {} into ee={} : {}'.format(primitive_name
, ee_id
, e
),
675 primitive_name
=primitive_name
678 async def disconnect(self
):
679 self
.log
.info('closing juju N2VC...')
680 await self
._juju
_logout
()
683 ##################################################################################################
684 ########################################## P R I V A T E #########################################
685 ##################################################################################################
694 # write ee_id to database: _admin.deployed.VCA.x
696 the_table
= db_dict
['collection']
697 the_filter
= db_dict
['filter']
698 the_path
= db_dict
['path']
699 if not the_path
[-1] == '.':
700 the_path
= the_path
+ '.'
701 update_dict
= {the_path
+ 'ee_id': ee_id
}
702 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
706 update_dict
=update_dict
,
709 except asyncio
.CancelledError
:
711 except Exception as e
:
712 self
.log
.error('Error writing ee_id to database: {}'.format(e
))
717 application_name
: str,
721 Build an execution environment id form model, application and machine
723 :param application_name:
727 # id for the execution environment
728 return '{}.{}.{}'.format(model_name
, application_name
, machine_id
)
731 def _get_ee_id_components(
733 ) -> (str, str, str):
735 Get model, application and machine components from an execution environment id
737 :return: model_name, application_name, machine_id
741 return None, None, None
743 # split components of id
744 parts
= ee_id
.split('.')
745 model_name
= parts
[0]
746 application_name
= parts
[1]
747 machine_id
= parts
[2]
748 return model_name
, application_name
, machine_id
750 def _get_application_name(self
, namespace
: str) -> str:
752 Build application name from namespace
754 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
757 # TODO: Enforce the Juju 50-character application limit
759 # split namespace components
760 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(namespace
=namespace
)
762 if vnf_id
is None or len(vnf_id
) == 0:
765 # Shorten the vnf_id to its last twelve characters
766 vnf_id
= 'vnf-' + vnf_id
[-12:]
768 if vdu_id
is None or len(vdu_id
) == 0:
771 # Shorten the vdu_id to its last twelve characters
772 vdu_id
= '-vdu-' + vdu_id
[-12:]
774 if vdu_count
is None or len(vdu_count
) == 0:
777 vdu_count
= '-cnt-' + vdu_count
779 application_name
= 'app-{}{}{}'.format(vnf_id
, vdu_id
, vdu_count
)
781 return N2VCJujuConnector
._format
_app
_name
(application_name
)
783 async def _juju_create_machine(
786 application_name
: str,
787 machine_id
: str = None,
788 db_dict
: dict = None,
789 progress_timeout
: float = None,
790 total_timeout
: float = None
793 self
.log
.debug('creating machine in model: {}, existing machine id: {}'.format(model_name
, machine_id
))
795 # get juju model and observer (create model if needed)
796 model
= await self
._juju
_get
_model
(model_name
=model_name
)
797 observer
= self
.juju_observers
[model_name
]
799 # find machine id in model
801 if machine_id
is not None:
802 self
.log
.debug('Finding existing machine id {} in model'.format(machine_id
))
803 # get juju existing machines in the model
804 existing_machines
= await model
.get_machines()
805 if machine_id
in existing_machines
:
806 self
.log
.debug('Machine id {} found in model (reusing it)'.format(machine_id
))
807 machine
= model
.machines
[machine_id
]
810 self
.log
.debug('Creating a new machine in juju...')
811 # machine does not exist, create it and wait for it
812 machine
= await model
.add_machine(
819 # register machine with observer
820 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
822 # id for the execution environment
823 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
824 model_name
=model_name
,
825 application_name
=application_name
,
826 machine_id
=str(machine
.entity_id
)
829 # write ee_id in database
830 self
._write
_ee
_id
_db
(
835 # wait for machine creation
836 await observer
.wait_for_machine(
837 machine_id
=str(machine
.entity_id
),
838 progress_timeout
=progress_timeout
,
839 total_timeout
=total_timeout
844 self
.log
.debug('Reusing old machine pending')
846 # register machine with observer
847 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
849 # machine does exist, but it is in creation process (pending), wait for create finalisation
850 await observer
.wait_for_machine(
851 machine_id
=machine
.entity_id
,
852 progress_timeout
=progress_timeout
,
853 total_timeout
=total_timeout
)
855 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
858 async def _juju_provision_machine(
863 private_key_path
: str,
864 db_dict
: dict = None,
865 progress_timeout
: float = None,
866 total_timeout
: float = None
869 if not self
.api_proxy
:
870 msg
= 'Cannot provision machine: api_proxy is not defined'
871 self
.log
.error(msg
=msg
)
872 raise N2VCException(message
=msg
)
874 self
.log
.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name
, hostname
, username
))
876 if not self
._authenticated
:
877 await self
._juju
_login
()
879 # get juju model and observer
880 model
= await self
._juju
_get
_model
(model_name
=model_name
)
881 observer
= self
.juju_observers
[model_name
]
883 # TODO check if machine is already provisioned
884 machine_list
= await model
.get_machines()
886 provisioner
= SSHProvisioner(
889 private_key_path
=private_key_path
,
895 params
= provisioner
.provision_machine()
896 except Exception as ex
:
897 msg
= "Exception provisioning machine: {}".format(ex
)
899 raise N2VCException(message
=msg
)
901 params
.jobs
= ['JobHostUnits']
903 connection
= model
.connection()
905 # Submit the request.
906 self
.log
.debug("Adding machine to model")
907 client_facade
= client
.ClientFacade
.from_connection(connection
)
908 results
= await client_facade
.AddMachines(params
=[params
])
909 error
= results
.machines
[0].error
911 msg
= "Error adding machine: {}}".format(error
.message
)
912 self
.log
.error(msg
=msg
)
913 raise ValueError(msg
)
915 machine_id
= results
.machines
[0].machine
917 # Need to run this after AddMachines has been called,
918 # as we need the machine_id
919 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
920 asyncio
.ensure_future(provisioner
.install_agent(
921 connection
=connection
,
923 machine_id
=machine_id
,
927 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
930 machine_list
= await model
.get_machines()
931 if machine_id
in machine_list
:
932 self
.log
.debug('Machine {} found in model!'.format(machine_id
))
933 machine
= model
.machines
.get(machine_id
)
935 await asyncio
.sleep(2)
938 msg
= 'Machine {} not found in model'.format(machine_id
)
939 self
.log
.error(msg
=msg
)
942 # register machine with observer
943 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
945 # wait for machine creation
946 self
.log
.debug('waiting for provision finishes... {}'.format(machine_id
))
947 await observer
.wait_for_machine(
948 machine_id
=machine_id
,
949 progress_timeout
=progress_timeout
,
950 total_timeout
=total_timeout
953 self
.log
.debug("Machine provisioned {}".format(machine_id
))
957 async def _juju_deploy_charm(
960 application_name
: str,
964 progress_timeout
: float = None,
965 total_timeout
: float = None,
967 ) -> (Application
, int):
969 # get juju model and observer
970 model
= await self
._juju
_get
_model
(model_name
=model_name
)
971 observer
= self
.juju_observers
[model_name
]
973 # check if application already exists
975 if application_name
in model
.applications
:
976 application
= model
.applications
[application_name
]
978 if application
is None:
980 # application does not exist, create it and wait for it
981 self
.log
.debug('deploying application {} to machine {}, model {}'
982 .format(application_name
, machine_id
, model_name
))
983 self
.log
.debug('charm: {}'.format(charm_path
))
986 application
= await model
.deploy(
987 entity_url
=charm_path
,
988 application_name
=application_name
,
996 # register application with observer
997 observer
.register_application(application
=application
, db_dict
=db_dict
)
999 self
.log
.debug('waiting for application deployed... {}'.format(application
.entity_id
))
1000 retries
= await observer
.wait_for_application(
1001 application_id
=application
.entity_id
,
1002 progress_timeout
=progress_timeout
,
1003 total_timeout
=total_timeout
)
1004 self
.log
.debug('application deployed')
1008 # register application with observer
1009 observer
.register_application(application
=application
, db_dict
=db_dict
)
1011 # application already exists, but not finalised
1012 self
.log
.debug('application already exists, waiting for deployed...')
1013 retries
= await observer
.wait_for_application(
1014 application_id
=application
.entity_id
,
1015 progress_timeout
=progress_timeout
,
1016 total_timeout
=total_timeout
)
1017 self
.log
.debug('application deployed')
1019 return application
, retries
1021 async def _juju_execute_action(
1024 application_name
: str,
1027 progress_timeout
: float = None,
1028 total_timeout
: float = None,
1032 # get juju model and observer
1033 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1034 observer
= self
.juju_observers
[model_name
]
1036 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1039 for u
in application
.units
:
1040 if await u
.is_leader_from_status():
1042 if unit
is not None:
1043 actions
= await application
.get_actions()
1044 if action_name
in actions
:
1045 self
.log
.debug('executing action "{}" using params: {}'.format(action_name
, kwargs
))
1046 action
= await unit
.run_action(action_name
, **kwargs
)
1048 # register action with observer
1049 observer
.register_action(action
=action
, db_dict
=db_dict
)
1051 await observer
.wait_for_action(
1052 action_id
=action
.entity_id
,
1053 progress_timeout
=progress_timeout
,
1054 total_timeout
=total_timeout
)
1055 self
.log
.debug('action completed with status: {}'.format(action
.status
))
1056 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1057 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1058 if action
.entity_id
in status
:
1059 status
= status
[action
.entity_id
]
1062 return output
, status
1064 raise N2VCExecutionException(
1065 message
='Cannot execute action on charm',
1066 primitive_name
=action_name
1069 async def _juju_configure_application(
1072 application_name
: str,
1075 progress_timeout
: float = None,
1076 total_timeout
: float = None
1079 # get the application
1080 application
= await self
._juju
_get
_application
(model_name
=model_name
, application_name
=application_name
)
1082 self
.log
.debug('configuring the application {} -> {}'.format(application_name
, config
))
1083 res
= await application
.set_config(config
)
1084 self
.log
.debug('application {} configured. res={}'.format(application_name
, res
))
1086 # Verify the config is set
1087 new_conf
= await application
.get_config()
1089 value
= new_conf
[key
]['value']
1090 self
.log
.debug(' {} = {}'.format(key
, value
))
1091 if config
[key
] != value
:
1092 raise N2VCException(
1093 message
='key {} is not configured correctly {} != {}'.format(key
, config
[key
], new_conf
[key
])
1096 # check if 'verify-ssh-credentials' action exists
1097 # unit = application.units[0]
1098 actions
= await application
.get_actions()
1099 if 'verify-ssh-credentials' not in actions
:
1100 msg
= 'Action verify-ssh-credentials does not exist in application {}'.format(application_name
)
1101 self
.log
.debug(msg
=msg
)
1104 # execute verify-credentials
1106 retry_timeout
= 15.0
1107 for i
in range(num_retries
):
1109 self
.log
.debug('Executing action verify-ssh-credentials...')
1110 output
, ok
= await self
._juju
_execute
_action
(
1111 model_name
=model_name
,
1112 application_name
=application_name
,
1113 action_name
='verify-ssh-credentials',
1115 progress_timeout
=progress_timeout
,
1116 total_timeout
=total_timeout
1118 self
.log
.debug('Result: {}, output: {}'.format(ok
, output
))
1120 except asyncio
.CancelledError
:
1122 except Exception as e
:
1123 self
.log
.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e
))
1124 await asyncio
.sleep(retry_timeout
)
1126 self
.log
.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries
))
1129 async def _juju_get_application(
1132 application_name
: str
1134 """Get the deployed application."""
1136 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1138 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1140 if model
.applications
and application_name
in model
.applications
:
1141 return model
.applications
[application_name
]
1143 raise N2VCException(message
='Cannot get application {} from model {}'.format(application_name
, model_name
))
1145 async def _juju_get_model(self
, model_name
: str) -> Model
:
1146 """ Get a model object from juju controller
1147 If the model does not exits, it creates it.
1149 :param str model_name: name of the model
1150 :returns Model: model obtained from juju controller or Exception
1154 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1156 if model_name
in self
.juju_models
:
1157 return self
.juju_models
[model_name
]
1159 if self
._creating
_model
:
1160 self
.log
.debug('Another coroutine is creating a model. Wait...')
1161 while self
._creating
_model
:
1162 # another coroutine is creating a model, wait
1163 await asyncio
.sleep(0.1)
1164 # retry (perhaps another coroutine has created the model meanwhile)
1165 if model_name
in self
.juju_models
:
1166 return self
.juju_models
[model_name
]
1169 self
._creating
_model
= True
1171 # get juju model names from juju
1172 model_list
= await self
.controller
.list_models()
1174 if model_name
not in model_list
:
1175 self
.log
.info('Model {} does not exist. Creating new model...'.format(model_name
))
1176 config_dict
= {'authorized-keys': self
.public_key
}
1178 config_dict
['apt-mirror'] = self
.apt_mirror
1179 if not self
.enable_os_upgrade
:
1180 config_dict
['enable-os-refresh-update'] = False
1181 config_dict
['enable-os-upgrade'] = False
1182 if self
.cloud
in self
.BUILT_IN_CLOUDS
:
1183 model
= await self
.controller
.add_model(
1184 model_name
=model_name
,
1186 cloud_name
=self
.cloud
,
1189 model
= await self
.controller
.add_model(
1190 model_name
=model_name
,
1192 cloud_name
=self
.cloud
,
1193 credential_name
="admin"
1195 self
.log
.info('New model created, name={}'.format(model_name
))
1197 self
.log
.debug('Model already exists in juju. Getting model {}'.format(model_name
))
1198 model
= await self
.controller
.get_model(model_name
)
1199 self
.log
.debug('Existing model in juju, name={}'.format(model_name
))
1201 self
.juju_models
[model_name
] = model
1202 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1205 except Exception as e
:
1206 msg
= 'Cannot get model {}. Exception: {}'.format(model_name
, e
)
1208 raise N2VCException(msg
)
1210 self
._creating
_model
= False
1212 async def _juju_add_relation(
1215 application_name_1
: str,
1216 application_name_2
: str,
1221 # get juju model and observer
1222 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1224 r1
= '{}:{}'.format(application_name_1
, relation_1
)
1225 r2
= '{}:{}'.format(application_name_2
, relation_2
)
1227 self
.log
.debug('adding relation: {} -> {}'.format(r1
, r2
))
1229 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1230 except JujuAPIError
as e
:
1231 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1232 # let the operation fail silently.
1233 if 'not found' in e
.message
:
1235 if 'already exists' in e
.message
:
1237 # another execption, raise it
1240 async def _juju_destroy_application(
1243 application_name
: str
1246 self
.log
.debug('Destroying application {} in model {}'.format(application_name
, model_name
))
1248 # get juju model and observer
1249 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1250 observer
= self
.juju_observers
[model_name
]
1252 application
= model
.applications
.get(application_name
)
1254 observer
.unregister_application(application_name
)
1255 await application
.destroy()
1257 self
.log
.debug('Application not found: {}'.format(application_name
))
1259 async def _juju_destroy_machine(
1263 total_timeout
: float = None
1266 self
.log
.debug('Destroying machine {} in model {}'.format(machine_id
, model_name
))
1268 if total_timeout
is None:
1269 total_timeout
= 3600
1271 # get juju model and observer
1272 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1273 observer
= self
.juju_observers
[model_name
]
1275 machines
= await model
.get_machines()
1276 if machine_id
in machines
:
1277 machine
= model
.machines
[machine_id
]
1278 observer
.unregister_machine(machine_id
)
1279 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1280 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1282 ].startswith("manual:"):
1283 self
.log
.debug("machine.destroy(force=True) started.")
1284 await machine
.destroy(force
=True)
1285 self
.log
.debug("machine.destroy(force=True) passed.")
1287 end
= time
.time() + total_timeout
1288 # wait for machine removal
1289 machines
= await model
.get_machines()
1290 while machine_id
in machines
and time
.time() < end
:
1291 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
1292 await asyncio
.sleep(0.5)
1293 machines
= await model
.get_machines()
1294 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1296 self
.log
.debug('Machine not found: {}'.format(machine_id
))
1298 async def _juju_destroy_model(
1301 total_timeout
: float = None
1304 self
.log
.debug('Destroying model {}'.format(model_name
))
1306 if total_timeout
is None:
1307 total_timeout
= 3600
1308 end
= time
.time() + total_timeout
1310 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1314 message
="Model {} does not exist".format(model_name
)
1317 uuid
= model
.info
.uuid
1319 # destroy applications
1320 for application_name
in model
.applications
:
1322 await self
._juju
_destroy
_application
(model_name
=model_name
, application_name
=application_name
)
1323 except Exception as e
:
1325 "Error destroying application {} in model {}: {}".format(
1333 machines
= await model
.get_machines()
1334 for machine_id
in machines
:
1336 await self
._juju
_destroy
_machine
(model_name
=model_name
, machine_id
=machine_id
)
1337 except asyncio
.CancelledError
:
1339 except Exception as e
:
1340 # ignore exceptions destroying machine
1343 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1345 self
.log
.debug('destroying model {}...'.format(model_name
))
1346 await self
.controller
.destroy_model(uuid
)
1347 # self.log.debug('model destroy requested {}'.format(model_name))
1349 # wait for model is completely destroyed
1350 self
.log
.debug('Waiting for model {} to be destroyed...'.format(model_name
))
1352 while time
.time() < end
:
1354 # await self.controller.get_model(uuid)
1355 models
= await self
.controller
.list_models()
1356 if model_name
not in models
:
1357 self
.log
.debug('The model {} ({}) was destroyed'.format(model_name
, uuid
))
1359 except asyncio
.CancelledError
:
1361 except Exception as e
:
1363 await asyncio
.sleep(5)
1364 raise N2VCException("Timeout waiting for model {} to be destroyed {}".format(model_name
, last_exception
))
1366 async def _juju_login(self
):
1367 """Connect to juju controller
1371 # if already authenticated, exit function
1372 if self
._authenticated
:
1375 # if connecting, wait for finish
1376 # another task could be trying to connect in parallel
1377 while self
._connecting
:
1378 await asyncio
.sleep(0.1)
1380 # double check after other task has finished
1381 if self
._authenticated
:
1385 self
._connecting
= True
1387 'connecting to juju controller: {} {}:{}{}'
1388 .format(self
.url
, self
.username
, self
.secret
[:8] + '...', ' with ca_cert' if self
.ca_cert
else ''))
1390 # Create controller object
1391 self
.controller
= Controller(loop
=self
.loop
)
1392 # Connect to controller
1393 await self
.controller
.connect(
1395 username
=self
.username
,
1396 password
=self
.secret
,
1399 self
._authenticated
= True
1400 self
.log
.info('juju controller connected')
1401 except Exception as e
:
1402 message
= 'Exception connecting to juju: {}'.format(e
)
1403 self
.log
.error(message
)
1404 raise N2VCConnectionException(
1409 self
._connecting
= False
1411 async def _juju_logout(self
):
1412 """Logout of the Juju controller."""
1413 if not self
._authenticated
:
1416 # disconnect all models
1417 for model_name
in self
.juju_models
:
1419 await self
._juju
_disconnect
_model
(model_name
)
1420 except Exception as e
:
1421 self
.log
.error('Error disconnecting model {} : {}'.format(model_name
, e
))
1422 # continue with next model...
1424 self
.log
.info("Disconnecting controller")
1426 await self
.controller
.disconnect()
1427 except Exception as e
:
1428 raise N2VCConnectionException(message
='Error disconnecting controller: {}'.format(e
), url
=self
.url
)
1430 self
.controller
= None
1431 self
._authenticated
= False
1432 self
.log
.info('disconnected')
1434 async def _juju_disconnect_model(
1438 self
.log
.debug("Disconnecting model {}".format(model_name
))
1439 if model_name
in self
.juju_models
:
1440 await self
.juju_models
[model_name
].disconnect()
1441 self
.juju_models
[model_name
] = None
1442 self
.juju_observers
[model_name
] = None
1444 self
.warning('Cannot disconnect model: {}'.format(model_name
))
1446 def _create_juju_public_key(self
):
1447 """Recreate the Juju public key on lcm container, if needed
1448 Certain libjuju commands expect to be run from the same machine as Juju
1449 is bootstrapped to. This method will write the public key to disk in
1450 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1453 # Make sure that we have a public key before writing to disk
1454 if self
.public_key
is None or len(self
.public_key
) == 0:
1455 if 'OSMLCM_VCA_PUBKEY' in os
.environ
:
1456 self
.public_key
= os
.getenv('OSMLCM_VCA_PUBKEY', '')
1457 if len(self
.public_key
) == 0:
1462 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser('~'))
1463 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1464 self
.log
.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path
, self
.public_key
))
1465 if not os
.path
.exists(pk_path
):
1466 # create path and write file
1467 os
.makedirs(pk_path
)
1468 with
open(file_path
, 'w') as f
:
1469 self
.log
.debug('Creating juju public key file: {}'.format(file_path
))
1470 f
.write(self
.public_key
)
1472 self
.log
.debug('juju public key file already exists: {}'.format(file_path
))
1475 def _format_model_name(name
: str) -> str:
1476 """Format the name of the model.
1478 Model names may only contain lowercase letters, digits and hyphens
1481 return name
.replace('_', '-').replace(' ', '-').lower()
1484 def _format_app_name(name
: str) -> str:
1485 """Format the name of the application (in order to assure valid application name).
1487 Application names have restrictions (run juju deploy --help):
1488 - contains lowercase letters 'a'-'z'
1489 - contains numbers '0'-'9'
1490 - contains hyphens '-'
1491 - starts with a lowercase letter
1492 - not two or more consecutive hyphens
1493 - after a hyphen, not a group with all numbers
1496 def all_numbers(s
: str) -> bool:
1502 new_name
= name
.replace('_', '-')
1503 new_name
= new_name
.replace(' ', '-')
1504 new_name
= new_name
.lower()
1505 while new_name
.find('--') >= 0:
1506 new_name
= new_name
.replace('--', '-')
1507 groups
= new_name
.split('-')
1509 # find 'all numbers' groups and prefix them with a letter
1511 for i
in range(len(groups
)):
1513 if all_numbers(group
):
1519 if app_name
[0].isdigit():
1520 app_name
= 'z' + app_name