Fix bug 1039: Add config to install_configuration_sw
[osm/N2VC.git] / n2vc / n2vc_juju_conn.py
1 ##
2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
4 # All Rights Reserved.
5 #
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
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
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
15 # implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
21 ##
22
23 import logging
24 import os
25 import asyncio
26 import time
27 import base64
28 import binascii
29 import re
30
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
37
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
45
46 from n2vc.provisioner import SSHProvisioner
47
48
49 class N2VCJujuConnector(N2VCConnector):
50
51 """
52 ##################################################################################################
53 ########################################## P U B L I C ###########################################
54 ##################################################################################################
55 """
56
57 def __init__(
58 self,
59 db: object,
60 fs: object,
61 log: object = None,
62 loop: object = None,
63 url: str = '127.0.0.1:17070',
64 username: str = 'admin',
65 vca_config: dict = None,
66 on_update_db=None
67 ):
68 """Initialize juju N2VC connector
69 """
70
71 # parent class constructor
72 N2VCConnector.__init__(
73 self,
74 db=db,
75 fs=fs,
76 log=log,
77 loop=loop,
78 url=url,
79 username=username,
80 vca_config=vca_config,
81 on_update_db=on_update_db
82 )
83
84 # silence websocket traffic log
85 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
86 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
87 logging.getLogger('model').setLevel(logging.WARN)
88
89 self.log.info('Initializing N2VC juju connector...')
90
91 """
92 ##############################################################
93 # check arguments
94 ##############################################################
95 """
96
97 # juju URL
98 if url is None:
99 raise N2VCBadArgumentsException('Argument url is mandatory', ['url'])
100 url_parts = url.split(':')
101 if len(url_parts) != 2:
102 raise N2VCBadArgumentsException('Argument url: bad format (localhost:port) -> {}'.format(url), ['url'])
103 self.hostname = url_parts[0]
104 try:
105 self.port = int(url_parts[1])
106 except ValueError:
107 raise N2VCBadArgumentsException('url port must be a number -> {}'.format(url), ['url'])
108
109 # juju USERNAME
110 if username is None:
111 raise N2VCBadArgumentsException('Argument username is mandatory', ['username'])
112
113 # juju CONFIGURATION
114 if vca_config is None:
115 raise N2VCBadArgumentsException('Argument vca_config is mandatory', ['vca_config'])
116
117 if 'secret' in vca_config:
118 self.secret = vca_config['secret']
119 else:
120 raise N2VCBadArgumentsException('Argument vca_config.secret is mandatory', ['vca_config.secret'])
121
122 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
123 # if exists, it will be written in lcm container: _create_juju_public_key()
124 if 'public_key' in vca_config:
125 self.public_key = vca_config['public_key']
126 else:
127 self.public_key = None
128
129 # TODO: Verify ca_cert is valid before using. VCA will crash
130 # if the ca_cert isn't formatted correctly.
131 def base64_to_cacert(b64string):
132 """Convert the base64-encoded string containing the VCA CACERT.
133
134 The input string....
135
136 """
137 try:
138 cacert = base64.b64decode(b64string).decode("utf-8")
139
140 cacert = re.sub(
141 r'\\n',
142 r'\n',
143 cacert,
144 )
145 except binascii.Error as e:
146 self.log.debug("Caught binascii.Error: {}".format(e))
147 raise N2VCInvalidCertificate(message="Invalid CA Certificate")
148
149 return cacert
150
151 self.ca_cert = vca_config.get('ca_cert')
152 if self.ca_cert:
153 self.ca_cert = base64_to_cacert(vca_config['ca_cert'])
154
155 if 'api_proxy' in vca_config:
156 self.api_proxy = vca_config['api_proxy']
157 self.log.debug('api_proxy for native charms configured: {}'.format(self.api_proxy))
158 else:
159 self.warning('api_proxy is not configured. Support for native charms is disabled')
160
161 if 'enable_os_upgrade' in vca_config:
162 self.enable_os_upgrade = vca_config['enable_os_upgrade']
163 else:
164 self.enable_os_upgrade = True
165
166 if 'apt_mirror' in vca_config:
167 self.apt_mirror = vca_config['apt_mirror']
168 else:
169 self.apt_mirror = None
170
171 self.cloud = vca_config.get('cloud')
172 self.log.debug('Arguments have been checked')
173
174 # juju data
175 self.controller = None # it will be filled when connect to juju
176 self.juju_models = {} # model objects for every model_name
177 self.juju_observers = {} # model observers for every model_name
178 self._connecting = False # while connecting to juju (to avoid duplicate connections)
179 self._authenticated = False # it will be True when juju connection be stablished
180 self._creating_model = False # True during model creation
181
182 # create juju pub key file in lcm container at ./local/share/juju/ssh/juju_id_rsa.pub
183 self._create_juju_public_key()
184
185 self.log.info('N2VC juju connector initialized')
186
187 async def get_status(self, namespace: str, yaml_format: bool = True):
188
189 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
190
191 if not self._authenticated:
192 await self._juju_login()
193
194 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
195 # model name is ns_id
196 model_name = ns_id
197 if model_name is None:
198 msg = 'Namespace {} not valid'.format(namespace)
199 self.log.error(msg)
200 raise N2VCBadArgumentsException(msg, ['namespace'])
201
202 # get juju model (create model if needed)
203 model = await self._juju_get_model(model_name=model_name)
204
205 status = await model.get_status()
206
207 if yaml_format:
208 return obj_to_yaml(status)
209 else:
210 return obj_to_dict(status)
211
212 async def create_execution_environment(
213 self,
214 namespace: str,
215 db_dict: dict,
216 reuse_ee_id: str = None,
217 progress_timeout: float = None,
218 total_timeout: float = None
219 ) -> (str, dict):
220
221 self.log.info('Creating execution environment. namespace: {}, reuse_ee_id: {}'.format(namespace, reuse_ee_id))
222
223 if not self._authenticated:
224 await self._juju_login()
225
226 machine_id = None
227 if reuse_ee_id:
228 model_name, application_name, machine_id = self._get_ee_id_components(ee_id=reuse_ee_id)
229 else:
230 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
231 # model name is ns_id
232 model_name = ns_id
233 # application name
234 application_name = self._get_application_name(namespace=namespace)
235
236 self.log.debug('model name: {}, application name: {}, machine_id: {}'
237 .format(model_name, application_name, machine_id))
238
239 # create or reuse a new juju machine
240 try:
241 machine = await self._juju_create_machine(
242 model_name=model_name,
243 application_name=application_name,
244 machine_id=machine_id,
245 db_dict=db_dict,
246 progress_timeout=progress_timeout,
247 total_timeout=total_timeout
248 )
249 except Exception as e:
250 message = 'Error creating machine on juju: {}'.format(e)
251 self.log.error(message)
252 raise N2VCException(message=message)
253
254 # id for the execution environment
255 ee_id = N2VCJujuConnector._build_ee_id(
256 model_name=model_name,
257 application_name=application_name,
258 machine_id=str(machine.entity_id)
259 )
260 self.log.debug('ee_id: {}'.format(ee_id))
261
262 # new machine credentials
263 credentials = dict()
264 credentials['hostname'] = machine.dns_name
265
266 self.log.info('Execution environment created. ee_id: {}, credentials: {}'.format(ee_id, credentials))
267
268 return ee_id, credentials
269
270 async def register_execution_environment(
271 self,
272 namespace: str,
273 credentials: dict,
274 db_dict: dict,
275 progress_timeout: float = None,
276 total_timeout: float = None
277 ) -> str:
278
279 if not self._authenticated:
280 await self._juju_login()
281
282 self.log.info('Registering execution environment. namespace={}, credentials={}'.format(namespace, credentials))
283
284 if credentials is None:
285 raise N2VCBadArgumentsException(message='credentials are mandatory', bad_args=['credentials'])
286 if credentials.get('hostname'):
287 hostname = credentials['hostname']
288 else:
289 raise N2VCBadArgumentsException(message='hostname is mandatory', bad_args=['credentials.hostname'])
290 if credentials.get('username'):
291 username = credentials['username']
292 else:
293 raise N2VCBadArgumentsException(message='username is mandatory', bad_args=['credentials.username'])
294 if 'private_key_path' in credentials:
295 private_key_path = credentials['private_key_path']
296 else:
297 # if not passed as argument, use generated private key path
298 private_key_path = self.private_key_path
299
300 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
301
302 # model name
303 model_name = ns_id
304 # application name
305 application_name = self._get_application_name(namespace=namespace)
306
307 # register machine on juju
308 try:
309 machine_id = await self._juju_provision_machine(
310 model_name=model_name,
311 hostname=hostname,
312 username=username,
313 private_key_path=private_key_path,
314 db_dict=db_dict,
315 progress_timeout=progress_timeout,
316 total_timeout=total_timeout
317 )
318 except Exception as e:
319 self.log.error('Error registering machine: {}'.format(e))
320 raise N2VCException(message='Error registering machine on juju: {}'.format(e))
321
322 self.log.info('Machine registered: {}'.format(machine_id))
323
324 # id for the execution environment
325 ee_id = N2VCJujuConnector._build_ee_id(
326 model_name=model_name,
327 application_name=application_name,
328 machine_id=str(machine_id)
329 )
330
331 self.log.info('Execution environment registered. ee_id: {}'.format(ee_id))
332
333 return ee_id
334
335 async def install_configuration_sw(
336 self,
337 ee_id: str,
338 artifact_path: str,
339 db_dict: dict,
340 progress_timeout: float = None,
341 total_timeout: float = None,
342 config: dict = None,
343 ):
344
345 self.log.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
346 .format(ee_id, artifact_path, db_dict))
347
348 if not self._authenticated:
349 await self._juju_login()
350
351 # check arguments
352 if ee_id is None or len(ee_id) == 0:
353 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
354 if artifact_path is None or len(artifact_path) == 0:
355 raise N2VCBadArgumentsException(message='artifact_path is mandatory', bad_args=['artifact_path'])
356 if db_dict is None:
357 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
358
359 try:
360 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
361 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
362 except Exception as e:
363 raise N2VCBadArgumentsException(
364 message='ee_id={} is not a valid execution environment id'.format(ee_id),
365 bad_args=['ee_id']
366 )
367
368 # remove // in charm path
369 while artifact_path.find('//') >= 0:
370 artifact_path = artifact_path.replace('//', '/')
371
372 # check charm path
373 if not self.fs.file_exists(artifact_path, mode="dir"):
374 msg = 'artifact path does not exist: {}'.format(artifact_path)
375 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
376
377 if artifact_path.startswith('/'):
378 full_path = self.fs.path + artifact_path
379 else:
380 full_path = self.fs.path + '/' + artifact_path
381
382 try:
383 application, retries = await self._juju_deploy_charm(
384 model_name=model_name,
385 application_name=application_name,
386 charm_path=full_path,
387 machine_id=machine_id,
388 db_dict=db_dict,
389 progress_timeout=progress_timeout,
390 total_timeout=total_timeout,
391 config=config
392 )
393 except Exception as e:
394 raise N2VCException(message='Error desploying charm into ee={} : {}'.format(ee_id, e))
395
396 self.log.info('Configuration sw installed')
397
398 async def get_ee_ssh_public__key(
399 self,
400 ee_id: str,
401 db_dict: dict,
402 progress_timeout: float = None,
403 total_timeout: float = None
404 ) -> str:
405
406 self.log.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id, db_dict))
407
408 if not self._authenticated:
409 await self._juju_login()
410
411 # check arguments
412 if ee_id is None or len(ee_id) == 0:
413 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
414 if db_dict is None:
415 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
416
417 try:
418 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
419 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
420 except Exception as e:
421 raise N2VCBadArgumentsException(
422 message='ee_id={} is not a valid execution environment id'.format(ee_id),
423 bad_args=['ee_id']
424 )
425
426 # try to execute ssh layer primitives (if exist):
427 # generate-ssh-key
428 # get-ssh-public-key
429
430 output = None
431
432 # execute action: generate-ssh-key
433 try:
434 output, status = await self._juju_execute_action(
435 model_name=model_name,
436 application_name=application_name,
437 action_name='generate-ssh-key',
438 db_dict=db_dict,
439 progress_timeout=progress_timeout,
440 total_timeout=total_timeout
441 )
442 except Exception as e:
443 self.log.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e))
444
445 # execute action: get-ssh-public-key
446 try:
447 output, status = await self._juju_execute_action(
448 model_name=model_name,
449 application_name=application_name,
450 action_name='get-ssh-public-key',
451 db_dict=db_dict,
452 progress_timeout=progress_timeout,
453 total_timeout=total_timeout
454 )
455 except Exception as e:
456 msg = 'Cannot execute action get-ssh-public-key: {}\n'.format(e)
457 self.log.info(msg)
458 raise e
459
460 # return public key if exists
461 return output["pubkey"] if "pubkey" in output else output
462
463 async def add_relation(
464 self,
465 ee_id_1: str,
466 ee_id_2: str,
467 endpoint_1: str,
468 endpoint_2: str
469 ):
470
471 self.log.debug('adding new relation between {} and {}, endpoints: {}, {}'
472 .format(ee_id_1, ee_id_2, endpoint_1, endpoint_2))
473
474 # check arguments
475 if not ee_id_1:
476 message = 'EE 1 is mandatory'
477 self.log.error(message)
478 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1'])
479 if not ee_id_2:
480 message = 'EE 2 is mandatory'
481 self.log.error(message)
482 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_2'])
483 if not endpoint_1:
484 message = 'endpoint 1 is mandatory'
485 self.log.error(message)
486 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_1'])
487 if not endpoint_2:
488 message = 'endpoint 2 is mandatory'
489 self.log.error(message)
490 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_2'])
491
492 if not self._authenticated:
493 await self._juju_login()
494
495 # get the model, the applications and the machines from the ee_id's
496 model_1, app_1, machine_1 = self._get_ee_id_components(ee_id_1)
497 model_2, app_2, machine_2 = self._get_ee_id_components(ee_id_2)
498
499 # model must be the same
500 if model_1 != model_2:
501 message = 'EE models are not the same: {} vs {}'.format(ee_id_1, ee_id_2)
502 self.log.error(message)
503 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1', 'ee_id_2'])
504
505 # add juju relations between two applications
506 try:
507 await self._juju_add_relation(
508 model_name=model_1,
509 application_name_1=app_1,
510 application_name_2=app_2,
511 relation_1=endpoint_1,
512 relation_2=endpoint_2
513 )
514 except Exception as e:
515 message = 'Error adding relation between {} and {}'.format(ee_id_1, ee_id_2)
516 self.log.error(message)
517 raise N2VCException(message=message)
518
519 async def remove_relation(
520 self
521 ):
522 if not self._authenticated:
523 await self._juju_login()
524 # TODO
525 self.log.info('Method not implemented yet')
526 raise NotImplemented()
527
528 async def deregister_execution_environments(
529 self
530 ):
531 if not self._authenticated:
532 await self._juju_login()
533 # TODO
534 self.log.info('Method not implemented yet')
535 raise NotImplemented()
536
537 async def delete_namespace(
538 self,
539 namespace: str,
540 db_dict: dict = None,
541 total_timeout: float = None
542 ):
543 self.log.info('Deleting namespace={}'.format(namespace))
544
545 if not self._authenticated:
546 await self._juju_login()
547
548 # check arguments
549 if namespace is None:
550 raise N2VCBadArgumentsException(message='namespace is mandatory', bad_args=['namespace'])
551
552 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
553 if ns_id is not None:
554 try:
555 await self._juju_destroy_model(
556 model_name=ns_id,
557 total_timeout=total_timeout
558 )
559 except Exception as e:
560 raise N2VCException(message='Error deleting namespace {} : {}'.format(namespace, e))
561 else:
562 raise N2VCBadArgumentsException(message='only ns_id is permitted to delete yet', bad_args=['namespace'])
563
564 self.log.info('Namespace {} deleted'.format(namespace))
565
566 async def delete_execution_environment(
567 self,
568 ee_id: str,
569 db_dict: dict = None,
570 total_timeout: float = None
571 ):
572 self.log.info('Deleting execution environment ee_id={}'.format(ee_id))
573
574 if not self._authenticated:
575 await self._juju_login()
576
577 # check arguments
578 if ee_id is None:
579 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
580
581 model_name, application_name, machine_id = self._get_ee_id_components(ee_id=ee_id)
582
583 # destroy the application
584 try:
585 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
586 except Exception as e:
587 raise N2VCException(message='Error deleting execution environment {} (application {}) : {}'
588 .format(ee_id, application_name, e))
589
590 # destroy the machine
591 # try:
592 # await self._juju_destroy_machine(
593 # model_name=model_name,
594 # machine_id=machine_id,
595 # total_timeout=total_timeout
596 # )
597 # except Exception as e:
598 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
599 # .format(ee_id, machine_id, e))
600
601 self.log.info('Execution environment {} deleted'.format(ee_id))
602
603 async def exec_primitive(
604 self,
605 ee_id: str,
606 primitive_name: str,
607 params_dict: dict,
608 db_dict: dict = None,
609 progress_timeout: float = None,
610 total_timeout: float = None
611 ) -> str:
612
613 self.log.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name, ee_id, params_dict))
614
615 if not self._authenticated:
616 await self._juju_login()
617
618 # check arguments
619 if ee_id is None or len(ee_id) == 0:
620 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
621 if primitive_name is None or len(primitive_name) == 0:
622 raise N2VCBadArgumentsException(message='action_name is mandatory', bad_args=['action_name'])
623 if params_dict is None:
624 params_dict = dict()
625
626 try:
627 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
628 except Exception:
629 raise N2VCBadArgumentsException(
630 message='ee_id={} is not a valid execution environment id'.format(ee_id),
631 bad_args=['ee_id']
632 )
633
634 if primitive_name == 'config':
635 # Special case: config primitive
636 try:
637 await self._juju_configure_application(
638 model_name=model_name,
639 application_name=application_name,
640 config=params_dict,
641 db_dict=db_dict,
642 progress_timeout=progress_timeout,
643 total_timeout=total_timeout
644 )
645 except Exception as e:
646 self.log.error('Error configuring juju application: {}'.format(e))
647 raise N2VCExecutionException(
648 message='Error configuring application into ee={} : {}'.format(ee_id, e),
649 primitive_name=primitive_name
650 )
651 return 'CONFIG OK'
652 else:
653 try:
654 output, status = await self._juju_execute_action(
655 model_name=model_name,
656 application_name=application_name,
657 action_name=primitive_name,
658 db_dict=db_dict,
659 progress_timeout=progress_timeout,
660 total_timeout=total_timeout,
661 **params_dict
662 )
663 if status == 'completed':
664 return output
665 else:
666 raise Exception('status is not completed: {}'.format(status))
667 except Exception as e:
668 self.log.error('Error executing primitive {}: {}'.format(primitive_name, e))
669 raise N2VCExecutionException(
670 message='Error executing primitive {} into ee={} : {}'.format(primitive_name, ee_id, e),
671 primitive_name=primitive_name
672 )
673
674 async def disconnect(self):
675 self.log.info('closing juju N2VC...')
676 await self._juju_logout()
677
678 """
679 ##################################################################################################
680 ########################################## P R I V A T E #########################################
681 ##################################################################################################
682 """
683
684 def _write_ee_id_db(
685 self,
686 db_dict: dict,
687 ee_id: str
688 ):
689
690 # write ee_id to database: _admin.deployed.VCA.x
691 try:
692 the_table = db_dict['collection']
693 the_filter = db_dict['filter']
694 the_path = db_dict['path']
695 if not the_path[-1] == '.':
696 the_path = the_path + '.'
697 update_dict = {the_path + 'ee_id': ee_id}
698 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
699 self.db.set_one(
700 table=the_table,
701 q_filter=the_filter,
702 update_dict=update_dict,
703 fail_on_empty=True
704 )
705 except Exception as e:
706 self.log.error('Error writing ee_id to database: {}'.format(e))
707
708 @staticmethod
709 def _build_ee_id(
710 model_name: str,
711 application_name: str,
712 machine_id: str
713 ):
714 """
715 Build an execution environment id form model, application and machine
716 :param model_name:
717 :param application_name:
718 :param machine_id:
719 :return:
720 """
721 # id for the execution environment
722 return '{}.{}.{}'.format(model_name, application_name, machine_id)
723
724 @staticmethod
725 def _get_ee_id_components(
726 ee_id: str
727 ) -> (str, str, str):
728 """
729 Get model, application and machine components from an execution environment id
730 :param ee_id:
731 :return: model_name, application_name, machine_id
732 """
733
734 if ee_id is None:
735 return None, None, None
736
737 # split components of id
738 parts = ee_id.split('.')
739 model_name = parts[0]
740 application_name = parts[1]
741 machine_id = parts[2]
742 return model_name, application_name, machine_id
743
744 def _get_application_name(self, namespace: str) -> str:
745 """
746 Build application name from namespace
747 :param namespace:
748 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
749 """
750
751 # TODO: Enforce the Juju 50-character application limit
752
753 # split namespace components
754 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
755
756 if vnf_id is None or len(vnf_id) == 0:
757 vnf_id = ''
758 else:
759 # Shorten the vnf_id to its last twelve characters
760 vnf_id = 'vnf-' + vnf_id[-12:]
761
762 if vdu_id is None or len(vdu_id) == 0:
763 vdu_id = ''
764 else:
765 # Shorten the vdu_id to its last twelve characters
766 vdu_id = '-vdu-' + vdu_id[-12:]
767
768 if vdu_count is None or len(vdu_count) == 0:
769 vdu_count = ''
770 else:
771 vdu_count = '-cnt-' + vdu_count
772
773 application_name = 'app-{}{}{}'.format(vnf_id, vdu_id, vdu_count)
774
775 return N2VCJujuConnector._format_app_name(application_name)
776
777 async def _juju_create_machine(
778 self,
779 model_name: str,
780 application_name: str,
781 machine_id: str = None,
782 db_dict: dict = None,
783 progress_timeout: float = None,
784 total_timeout: float = None
785 ) -> Machine:
786
787 self.log.debug('creating machine in model: {}, existing machine id: {}'.format(model_name, machine_id))
788
789 # get juju model and observer (create model if needed)
790 model = await self._juju_get_model(model_name=model_name)
791 observer = self.juju_observers[model_name]
792
793 # find machine id in model
794 machine = None
795 if machine_id is not None:
796 self.log.debug('Finding existing machine id {} in model'.format(machine_id))
797 # get juju existing machines in the model
798 existing_machines = await model.get_machines()
799 if machine_id in existing_machines:
800 self.log.debug('Machine id {} found in model (reusing it)'.format(machine_id))
801 machine = model.machines[machine_id]
802
803 if machine is None:
804 self.log.debug('Creating a new machine in juju...')
805 # machine does not exist, create it and wait for it
806 machine = await model.add_machine(
807 spec=None,
808 constraints=None,
809 disks=None,
810 series='xenial'
811 )
812
813 # register machine with observer
814 observer.register_machine(machine=machine, db_dict=db_dict)
815
816 # id for the execution environment
817 ee_id = N2VCJujuConnector._build_ee_id(
818 model_name=model_name,
819 application_name=application_name,
820 machine_id=str(machine.entity_id)
821 )
822
823 # write ee_id in database
824 self._write_ee_id_db(
825 db_dict=db_dict,
826 ee_id=ee_id
827 )
828
829 # wait for machine creation
830 await observer.wait_for_machine(
831 machine_id=str(machine.entity_id),
832 progress_timeout=progress_timeout,
833 total_timeout=total_timeout
834 )
835
836 else:
837
838 self.log.debug('Reusing old machine pending')
839
840 # register machine with observer
841 observer.register_machine(machine=machine, db_dict=db_dict)
842
843 # machine does exist, but it is in creation process (pending), wait for create finalisation
844 await observer.wait_for_machine(
845 machine_id=machine.entity_id,
846 progress_timeout=progress_timeout,
847 total_timeout=total_timeout)
848
849 self.log.debug("Machine ready at " + str(machine.dns_name))
850 return machine
851
852 async def _juju_provision_machine(
853 self,
854 model_name: str,
855 hostname: str,
856 username: str,
857 private_key_path: str,
858 db_dict: dict = None,
859 progress_timeout: float = None,
860 total_timeout: float = None
861 ) -> str:
862
863 if not self.api_proxy:
864 msg = 'Cannot provision machine: api_proxy is not defined'
865 self.log.error(msg=msg)
866 raise N2VCException(message=msg)
867
868 self.log.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name, hostname, username))
869
870 if not self._authenticated:
871 await self._juju_login()
872
873 # get juju model and observer
874 model = await self._juju_get_model(model_name=model_name)
875 observer = self.juju_observers[model_name]
876
877 # TODO check if machine is already provisioned
878 machine_list = await model.get_machines()
879
880 provisioner = SSHProvisioner(
881 host=hostname,
882 user=username,
883 private_key_path=private_key_path,
884 log=self.log
885 )
886
887 params = None
888 try:
889 params = provisioner.provision_machine()
890 except Exception as ex:
891 msg = "Exception provisioning machine: {}".format(ex)
892 self.log.error(msg)
893 raise N2VCException(message=msg)
894
895 params.jobs = ['JobHostUnits']
896
897 connection = model.connection()
898
899 # Submit the request.
900 self.log.debug("Adding machine to model")
901 client_facade = client.ClientFacade.from_connection(connection)
902 results = await client_facade.AddMachines(params=[params])
903 error = results.machines[0].error
904 if error:
905 msg = "Error adding machine: {}}".format(error.message)
906 self.log.error(msg=msg)
907 raise ValueError(msg)
908
909 machine_id = results.machines[0].machine
910
911 # Need to run this after AddMachines has been called,
912 # as we need the machine_id
913 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
914 asyncio.ensure_future(provisioner.install_agent(
915 connection=connection,
916 nonce=params.nonce,
917 machine_id=machine_id,
918 api=self.api_proxy,
919 ))
920
921 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
922 machine = None
923 for i in range(10):
924 machine_list = await model.get_machines()
925 if machine_id in machine_list:
926 self.log.debug('Machine {} found in model!'.format(machine_id))
927 machine = model.machines.get(machine_id)
928 break
929 await asyncio.sleep(2)
930
931 if machine is None:
932 msg = 'Machine {} not found in model'.format(machine_id)
933 self.log.error(msg=msg)
934 raise Exception(msg)
935
936 # register machine with observer
937 observer.register_machine(machine=machine, db_dict=db_dict)
938
939 # wait for machine creation
940 self.log.debug('waiting for provision finishes... {}'.format(machine_id))
941 await observer.wait_for_machine(
942 machine_id=machine_id,
943 progress_timeout=progress_timeout,
944 total_timeout=total_timeout
945 )
946
947 self.log.debug("Machine provisioned {}".format(machine_id))
948
949 return machine_id
950
951 async def _juju_deploy_charm(
952 self,
953 model_name: str,
954 application_name: str,
955 charm_path: str,
956 machine_id: str,
957 db_dict: dict,
958 progress_timeout: float = None,
959 total_timeout: float = None,
960 config: dict = None
961 ) -> (Application, int):
962
963 # get juju model and observer
964 model = await self._juju_get_model(model_name=model_name)
965 observer = self.juju_observers[model_name]
966
967 # check if application already exists
968 application = None
969 if application_name in model.applications:
970 application = model.applications[application_name]
971
972 if application is None:
973
974 # application does not exist, create it and wait for it
975 self.log.debug('deploying application {} to machine {}, model {}'
976 .format(application_name, machine_id, model_name))
977 self.log.debug('charm: {}'.format(charm_path))
978 series = 'xenial'
979 # series = None
980 application = await model.deploy(
981 entity_url=charm_path,
982 application_name=application_name,
983 channel='stable',
984 num_units=1,
985 series=series,
986 to=machine_id,
987 config=config
988 )
989
990 # register application with observer
991 observer.register_application(application=application, db_dict=db_dict)
992
993 self.log.debug('waiting for application deployed... {}'.format(application.entity_id))
994 retries = await observer.wait_for_application(
995 application_id=application.entity_id,
996 progress_timeout=progress_timeout,
997 total_timeout=total_timeout)
998 self.log.debug('application deployed')
999
1000 else:
1001
1002 # register application with observer
1003 observer.register_application(application=application, db_dict=db_dict)
1004
1005 # application already exists, but not finalised
1006 self.log.debug('application already exists, waiting for deployed...')
1007 retries = await observer.wait_for_application(
1008 application_id=application.entity_id,
1009 progress_timeout=progress_timeout,
1010 total_timeout=total_timeout)
1011 self.log.debug('application deployed')
1012
1013 return application, retries
1014
1015 async def _juju_execute_action(
1016 self,
1017 model_name: str,
1018 application_name: str,
1019 action_name: str,
1020 db_dict: dict,
1021 progress_timeout: float = None,
1022 total_timeout: float = None,
1023 **kwargs
1024 ) -> Action:
1025
1026 # get juju model and observer
1027 model = await self._juju_get_model(model_name=model_name)
1028 observer = self.juju_observers[model_name]
1029
1030 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1031
1032 unit = None
1033 for u in application.units:
1034 if await u.is_leader_from_status():
1035 unit = u
1036 if unit is not None:
1037 actions = await application.get_actions()
1038 if action_name in actions:
1039 self.log.debug('executing action "{}" using params: {}'.format(action_name, kwargs))
1040 action = await unit.run_action(action_name, **kwargs)
1041
1042 # register action with observer
1043 observer.register_action(action=action, db_dict=db_dict)
1044
1045 await observer.wait_for_action(
1046 action_id=action.entity_id,
1047 progress_timeout=progress_timeout,
1048 total_timeout=total_timeout)
1049 self.log.debug('action completed with status: {}'.format(action.status))
1050 output = await model.get_action_output(action_uuid=action.entity_id)
1051 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1052 if action.entity_id in status:
1053 status = status[action.entity_id]
1054 else:
1055 status = 'failed'
1056 return output, status
1057
1058 raise N2VCExecutionException(
1059 message='Cannot execute action on charm',
1060 primitive_name=action_name
1061 )
1062
1063 async def _juju_configure_application(
1064 self,
1065 model_name: str,
1066 application_name: str,
1067 config: dict,
1068 db_dict: dict,
1069 progress_timeout: float = None,
1070 total_timeout: float = None
1071 ):
1072
1073 # get the application
1074 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1075
1076 self.log.debug('configuring the application {} -> {}'.format(application_name, config))
1077 res = await application.set_config(config)
1078 self.log.debug('application {} configured. res={}'.format(application_name, res))
1079
1080 # Verify the config is set
1081 new_conf = await application.get_config()
1082 for key in config:
1083 value = new_conf[key]['value']
1084 self.log.debug(' {} = {}'.format(key, value))
1085 if config[key] != value:
1086 raise N2VCException(
1087 message='key {} is not configured correctly {} != {}'.format(key, config[key], new_conf[key])
1088 )
1089
1090 # check if 'verify-ssh-credentials' action exists
1091 # unit = application.units[0]
1092 actions = await application.get_actions()
1093 if 'verify-ssh-credentials' not in actions:
1094 msg = 'Action verify-ssh-credentials does not exist in application {}'.format(application_name)
1095 self.log.debug(msg=msg)
1096 return False
1097
1098 # execute verify-credentials
1099 num_retries = 20
1100 retry_timeout = 15.0
1101 for i in range(num_retries):
1102 try:
1103 self.log.debug('Executing action verify-ssh-credentials...')
1104 output, ok = await self._juju_execute_action(
1105 model_name=model_name,
1106 application_name=application_name,
1107 action_name='verify-ssh-credentials',
1108 db_dict=db_dict,
1109 progress_timeout=progress_timeout,
1110 total_timeout=total_timeout
1111 )
1112 self.log.debug('Result: {}, output: {}'.format(ok, output))
1113 return True
1114 except Exception as e:
1115 self.log.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e))
1116 await asyncio.sleep(retry_timeout)
1117 else:
1118 self.log.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries))
1119 return False
1120
1121 async def _juju_get_application(
1122 self,
1123 model_name: str,
1124 application_name: str
1125 ):
1126 """Get the deployed application."""
1127
1128 model = await self._juju_get_model(model_name=model_name)
1129
1130 application_name = N2VCJujuConnector._format_app_name(application_name)
1131
1132 if model.applications and application_name in model.applications:
1133 return model.applications[application_name]
1134 else:
1135 raise N2VCException(message='Cannot get application {} from model {}'.format(application_name, model_name))
1136
1137 async def _juju_get_model(self, model_name: str) -> Model:
1138 """ Get a model object from juju controller
1139 If the model does not exits, it creates it.
1140
1141 :param str model_name: name of the model
1142 :returns Model: model obtained from juju controller or Exception
1143 """
1144
1145 # format model name
1146 model_name = N2VCJujuConnector._format_model_name(model_name)
1147
1148 if model_name in self.juju_models:
1149 return self.juju_models[model_name]
1150
1151 if self._creating_model:
1152 self.log.debug('Another coroutine is creating a model. Wait...')
1153 while self._creating_model:
1154 # another coroutine is creating a model, wait
1155 await asyncio.sleep(0.1)
1156 # retry (perhaps another coroutine has created the model meanwhile)
1157 if model_name in self.juju_models:
1158 return self.juju_models[model_name]
1159
1160 try:
1161 self._creating_model = True
1162
1163 # get juju model names from juju
1164 model_list = await self.controller.list_models()
1165
1166 if model_name not in model_list:
1167 self.log.info('Model {} does not exist. Creating new model...'.format(model_name))
1168 config_dict = {'authorized-keys': self.public_key}
1169 if self.apt_mirror:
1170 config_dict['apt-mirror'] = self.apt_mirror
1171 if not self.enable_os_upgrade:
1172 config_dict['enable-os-refresh-update'] = False
1173 config_dict['enable-os-upgrade'] = False
1174
1175 model = await self.controller.add_model(
1176 model_name=model_name,
1177 config=config_dict,
1178 cloud_name=self.cloud,
1179 )
1180 self.log.info('New model created, name={}'.format(model_name))
1181 else:
1182 self.log.debug('Model already exists in juju. Getting model {}'.format(model_name))
1183 model = await self.controller.get_model(model_name)
1184 self.log.debug('Existing model in juju, name={}'.format(model_name))
1185
1186 self.juju_models[model_name] = model
1187 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1188 return model
1189
1190 except Exception as e:
1191 msg = 'Cannot get model {}. Exception: {}'.format(model_name, e)
1192 self.log.error(msg)
1193 raise N2VCException(msg)
1194 finally:
1195 self._creating_model = False
1196
1197 async def _juju_add_relation(
1198 self,
1199 model_name: str,
1200 application_name_1: str,
1201 application_name_2: str,
1202 relation_1: str,
1203 relation_2: str
1204 ):
1205
1206 # get juju model and observer
1207 model = await self._juju_get_model(model_name=model_name)
1208
1209 r1 = '{}:{}'.format(application_name_1, relation_1)
1210 r2 = '{}:{}'.format(application_name_2, relation_2)
1211
1212 self.log.debug('adding relation: {} -> {}'.format(r1, r2))
1213 try:
1214 await model.add_relation(relation1=r1, relation2=r2)
1215 except JujuAPIError as e:
1216 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1217 # let the operation fail silently.
1218 if 'not found' in e.message:
1219 return
1220 if 'already exists' in e.message:
1221 return
1222 # another execption, raise it
1223 raise e
1224
1225 async def _juju_destroy_application(
1226 self,
1227 model_name: str,
1228 application_name: str
1229 ):
1230
1231 self.log.debug('Destroying application {} in model {}'.format(application_name, model_name))
1232
1233 # get juju model and observer
1234 model = await self._juju_get_model(model_name=model_name)
1235 observer = self.juju_observers[model_name]
1236
1237 application = model.applications.get(application_name)
1238 if application:
1239 observer.unregister_application(application_name)
1240 await application.destroy()
1241 else:
1242 self.log.debug('Application not found: {}'.format(application_name))
1243
1244 async def _juju_destroy_machine(
1245 self,
1246 model_name: str,
1247 machine_id: str,
1248 total_timeout: float = None
1249 ):
1250
1251 self.log.debug('Destroying machine {} in model {}'.format(machine_id, model_name))
1252
1253 if total_timeout is None:
1254 total_timeout = 3600
1255
1256 # get juju model and observer
1257 model = await self._juju_get_model(model_name=model_name)
1258 observer = self.juju_observers[model_name]
1259
1260 machines = await model.get_machines()
1261 if machine_id in machines:
1262 machine = model.machines[machine_id]
1263 observer.unregister_machine(machine_id)
1264 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1265 if "instance-id" in machine.safe_data and machine.safe_data[
1266 "instance-id"
1267 ].startswith("manual:"):
1268 self.log.debug("machine.destroy(force=True) started.")
1269 await machine.destroy(force=True)
1270 self.log.debug("machine.destroy(force=True) passed.")
1271 # max timeout
1272 end = time.time() + total_timeout
1273 # wait for machine removal
1274 machines = await model.get_machines()
1275 while machine_id in machines and time.time() < end:
1276 self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
1277 await asyncio.sleep(0.5)
1278 machines = await model.get_machines()
1279 self.log.debug("Machine destroyed: {}".format(machine_id))
1280 else:
1281 self.log.debug('Machine not found: {}'.format(machine_id))
1282
1283 async def _juju_destroy_model(
1284 self,
1285 model_name: str,
1286 total_timeout: float = None
1287 ):
1288
1289 self.log.debug('Destroying model {}'.format(model_name))
1290
1291 if total_timeout is None:
1292 total_timeout = 3600
1293
1294 model = await self._juju_get_model(model_name=model_name)
1295
1296 if not model:
1297 raise N2VCException(
1298 message="Model {} does not exist".format(model_name)
1299 )
1300
1301 uuid = model.info.uuid
1302
1303 # destroy applications
1304 for application_name in model.applications:
1305 try:
1306 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
1307 except Exception as e:
1308 self.log.error(
1309 "Error destroying application {} in model {}: {}".format(
1310 application_name,
1311 model_name,
1312 e
1313 )
1314 )
1315
1316 # destroy machines
1317 machines = await model.get_machines()
1318 for machine_id in machines:
1319 try:
1320 await self._juju_destroy_machine(model_name=model_name, machine_id=machine_id)
1321 except Exception as e:
1322 # ignore exceptions destroying machine
1323 pass
1324
1325 await self._juju_disconnect_model(model_name=model_name)
1326
1327 self.log.debug('destroying model {}...'.format(model_name))
1328 await self.controller.destroy_model(uuid)
1329 self.log.debug('model destroy requested {}'.format(model_name))
1330
1331 # wait for model is completely destroyed
1332 end = time.time() + total_timeout
1333 while time.time() < end:
1334 self.log.debug('Waiting for model is destroyed...')
1335 try:
1336 # await self.controller.get_model(uuid)
1337 models = await self.controller.list_models()
1338 if model_name not in models:
1339 self.log.debug('The model {} ({}) was destroyed'.format(model_name, uuid))
1340 return
1341 except Exception as e:
1342 pass
1343 await asyncio.sleep(1.0)
1344
1345 async def _juju_login(self):
1346 """Connect to juju controller
1347
1348 """
1349
1350 # if already authenticated, exit function
1351 if self._authenticated:
1352 return
1353
1354 # if connecting, wait for finish
1355 # another task could be trying to connect in parallel
1356 while self._connecting:
1357 await asyncio.sleep(0.1)
1358
1359 # double check after other task has finished
1360 if self._authenticated:
1361 return
1362
1363 try:
1364 self._connecting = True
1365 self.log.info(
1366 'connecting to juju controller: {} {}:{} ca_cert: {}'
1367 .format(self.url, self.username, self.secret, '\n'+self.ca_cert if self.ca_cert else 'None'))
1368
1369 # Create controller object
1370 self.controller = Controller(loop=self.loop)
1371 # Connect to controller
1372 await self.controller.connect(
1373 endpoint=self.url,
1374 username=self.username,
1375 password=self.secret,
1376 cacert=self.ca_cert
1377 )
1378 self._authenticated = True
1379 self.log.info('juju controller connected')
1380 except Exception as e:
1381 message = 'Exception connecting to juju: {}'.format(e)
1382 self.log.error(message)
1383 raise N2VCConnectionException(
1384 message=message,
1385 url=self.url
1386 )
1387 finally:
1388 self._connecting = False
1389
1390 async def _juju_logout(self):
1391 """Logout of the Juju controller."""
1392 if not self._authenticated:
1393 return False
1394
1395 # disconnect all models
1396 for model_name in self.juju_models:
1397 try:
1398 await self._juju_disconnect_model(model_name)
1399 except Exception as e:
1400 self.log.error('Error disconnecting model {} : {}'.format(model_name, e))
1401 # continue with next model...
1402
1403 self.log.info("Disconnecting controller")
1404 try:
1405 await self.controller.disconnect()
1406 except Exception as e:
1407 raise N2VCConnectionException(message='Error disconnecting controller: {}'.format(e), url=self.url)
1408
1409 self.controller = None
1410 self._authenticated = False
1411 self.log.info('disconnected')
1412
1413 async def _juju_disconnect_model(
1414 self,
1415 model_name: str
1416 ):
1417 self.log.debug("Disconnecting model {}".format(model_name))
1418 if model_name in self.juju_models:
1419 await self.juju_models[model_name].disconnect()
1420 self.juju_models[model_name] = None
1421 self.juju_observers[model_name] = None
1422 else:
1423 self.warning('Cannot disconnect model: {}'.format(model_name))
1424
1425 def _create_juju_public_key(self):
1426 """Recreate the Juju public key on lcm container, if needed
1427 Certain libjuju commands expect to be run from the same machine as Juju
1428 is bootstrapped to. This method will write the public key to disk in
1429 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1430 """
1431
1432 # Make sure that we have a public key before writing to disk
1433 if self.public_key is None or len(self.public_key) == 0:
1434 if 'OSMLCM_VCA_PUBKEY' in os.environ:
1435 self.public_key = os.getenv('OSMLCM_VCA_PUBKEY', '')
1436 if len(self.public_key) == 0:
1437 return
1438 else:
1439 return
1440
1441 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser('~'))
1442 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1443 self.log.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path, self.public_key))
1444 if not os.path.exists(pk_path):
1445 # create path and write file
1446 os.makedirs(pk_path)
1447 with open(file_path, 'w') as f:
1448 self.log.debug('Creating juju public key file: {}'.format(file_path))
1449 f.write(self.public_key)
1450 else:
1451 self.log.debug('juju public key file already exists: {}'.format(file_path))
1452
1453 @staticmethod
1454 def _format_model_name(name: str) -> str:
1455 """Format the name of the model.
1456
1457 Model names may only contain lowercase letters, digits and hyphens
1458 """
1459
1460 return name.replace('_', '-').replace(' ', '-').lower()
1461
1462 @staticmethod
1463 def _format_app_name(name: str) -> str:
1464 """Format the name of the application (in order to assure valid application name).
1465
1466 Application names have restrictions (run juju deploy --help):
1467 - contains lowercase letters 'a'-'z'
1468 - contains numbers '0'-'9'
1469 - contains hyphens '-'
1470 - starts with a lowercase letter
1471 - not two or more consecutive hyphens
1472 - after a hyphen, not a group with all numbers
1473 """
1474
1475 def all_numbers(s: str) -> bool:
1476 for c in s:
1477 if not c.isdigit():
1478 return False
1479 return True
1480
1481 new_name = name.replace('_', '-')
1482 new_name = new_name.replace(' ', '-')
1483 new_name = new_name.lower()
1484 while new_name.find('--') >= 0:
1485 new_name = new_name.replace('--', '-')
1486 groups = new_name.split('-')
1487
1488 # find 'all numbers' groups and prefix them with a letter
1489 app_name = ''
1490 for i in range(len(groups)):
1491 group = groups[i]
1492 if all_numbers(group):
1493 group = 'z' + group
1494 if i > 0:
1495 app_name += '-'
1496 app_name += group
1497
1498 if app_name[0].isdigit():
1499 app_name = 'z' + app_name
1500
1501 return app_name