Fix bug 1049: Raise exception if model does not exist when destroying a model
[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 ):
343
344 self.log.info('Installing configuration sw on ee_id: {}, artifact path: {}, db_dict: {}'
345 .format(ee_id, artifact_path, db_dict))
346
347 if not self._authenticated:
348 await self._juju_login()
349
350 # check arguments
351 if ee_id is None or len(ee_id) == 0:
352 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
353 if artifact_path is None or len(artifact_path) == 0:
354 raise N2VCBadArgumentsException(message='artifact_path is mandatory', bad_args=['artifact_path'])
355 if db_dict is None:
356 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
357
358 try:
359 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
360 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
361 except Exception as e:
362 raise N2VCBadArgumentsException(
363 message='ee_id={} is not a valid execution environment id'.format(ee_id),
364 bad_args=['ee_id']
365 )
366
367 # remove // in charm path
368 while artifact_path.find('//') >= 0:
369 artifact_path = artifact_path.replace('//', '/')
370
371 # check charm path
372 if not self.fs.file_exists(artifact_path, mode="dir"):
373 msg = 'artifact path does not exist: {}'.format(artifact_path)
374 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
375
376 if artifact_path.startswith('/'):
377 full_path = self.fs.path + artifact_path
378 else:
379 full_path = self.fs.path + '/' + artifact_path
380
381 try:
382 application, retries = await self._juju_deploy_charm(
383 model_name=model_name,
384 application_name=application_name,
385 charm_path=full_path,
386 machine_id=machine_id,
387 db_dict=db_dict,
388 progress_timeout=progress_timeout,
389 total_timeout=total_timeout
390 )
391 except Exception as e:
392 raise N2VCException(message='Error desploying charm into ee={} : {}'.format(ee_id, e))
393
394 self.log.info('Configuration sw installed')
395
396 async def get_ee_ssh_public__key(
397 self,
398 ee_id: str,
399 db_dict: dict,
400 progress_timeout: float = None,
401 total_timeout: float = None
402 ) -> str:
403
404 self.log.info('Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}'.format(ee_id, db_dict))
405
406 if not self._authenticated:
407 await self._juju_login()
408
409 # check arguments
410 if ee_id is None or len(ee_id) == 0:
411 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
412 if db_dict is None:
413 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
414
415 try:
416 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
417 self.log.debug('model: {}, application: {}, machine: {}'.format(model_name, application_name, machine_id))
418 except Exception as e:
419 raise N2VCBadArgumentsException(
420 message='ee_id={} is not a valid execution environment id'.format(ee_id),
421 bad_args=['ee_id']
422 )
423
424 # try to execute ssh layer primitives (if exist):
425 # generate-ssh-key
426 # get-ssh-public-key
427
428 output = None
429
430 # execute action: generate-ssh-key
431 try:
432 output, status = await self._juju_execute_action(
433 model_name=model_name,
434 application_name=application_name,
435 action_name='generate-ssh-key',
436 db_dict=db_dict,
437 progress_timeout=progress_timeout,
438 total_timeout=total_timeout
439 )
440 except Exception as e:
441 self.log.info('Cannot execute action generate-ssh-key: {}\nContinuing...'.format(e))
442
443 # execute action: get-ssh-public-key
444 try:
445 output, status = await self._juju_execute_action(
446 model_name=model_name,
447 application_name=application_name,
448 action_name='get-ssh-public-key',
449 db_dict=db_dict,
450 progress_timeout=progress_timeout,
451 total_timeout=total_timeout
452 )
453 except Exception as e:
454 msg = 'Cannot execute action get-ssh-public-key: {}\n'.format(e)
455 self.log.info(msg)
456 raise e
457
458 # return public key if exists
459 return output["pubkey"] if "pubkey" in output else output
460
461 async def add_relation(
462 self,
463 ee_id_1: str,
464 ee_id_2: str,
465 endpoint_1: str,
466 endpoint_2: str
467 ):
468
469 self.log.debug('adding new relation between {} and {}, endpoints: {}, {}'
470 .format(ee_id_1, ee_id_2, endpoint_1, endpoint_2))
471
472 # check arguments
473 if not ee_id_1:
474 message = 'EE 1 is mandatory'
475 self.log.error(message)
476 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1'])
477 if not ee_id_2:
478 message = 'EE 2 is mandatory'
479 self.log.error(message)
480 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_2'])
481 if not endpoint_1:
482 message = 'endpoint 1 is mandatory'
483 self.log.error(message)
484 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_1'])
485 if not endpoint_2:
486 message = 'endpoint 2 is mandatory'
487 self.log.error(message)
488 raise N2VCBadArgumentsException(message=message, bad_args=['endpoint_2'])
489
490 if not self._authenticated:
491 await self._juju_login()
492
493 # get the model, the applications and the machines from the ee_id's
494 model_1, app_1, machine_1 = self._get_ee_id_components(ee_id_1)
495 model_2, app_2, machine_2 = self._get_ee_id_components(ee_id_2)
496
497 # model must be the same
498 if model_1 != model_2:
499 message = 'EE models are not the same: {} vs {}'.format(ee_id_1, ee_id_2)
500 self.log.error(message)
501 raise N2VCBadArgumentsException(message=message, bad_args=['ee_id_1', 'ee_id_2'])
502
503 # add juju relations between two applications
504 try:
505 await self._juju_add_relation(
506 model_name=model_1,
507 application_name_1=app_1,
508 application_name_2=app_2,
509 relation_1=endpoint_1,
510 relation_2=endpoint_2
511 )
512 except Exception as e:
513 message = 'Error adding relation between {} and {}'.format(ee_id_1, ee_id_2)
514 self.log.error(message)
515 raise N2VCException(message=message)
516
517 async def remove_relation(
518 self
519 ):
520 if not self._authenticated:
521 await self._juju_login()
522 # TODO
523 self.log.info('Method not implemented yet')
524 raise NotImplemented()
525
526 async def deregister_execution_environments(
527 self
528 ):
529 if not self._authenticated:
530 await self._juju_login()
531 # TODO
532 self.log.info('Method not implemented yet')
533 raise NotImplemented()
534
535 async def delete_namespace(
536 self,
537 namespace: str,
538 db_dict: dict = None,
539 total_timeout: float = None
540 ):
541 self.log.info('Deleting namespace={}'.format(namespace))
542
543 if not self._authenticated:
544 await self._juju_login()
545
546 # check arguments
547 if namespace is None:
548 raise N2VCBadArgumentsException(message='namespace is mandatory', bad_args=['namespace'])
549
550 nsi_id, ns_id, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
551 if ns_id is not None:
552 try:
553 await self._juju_destroy_model(
554 model_name=ns_id,
555 total_timeout=total_timeout
556 )
557 except Exception as e:
558 raise N2VCException(message='Error deleting namespace {} : {}'.format(namespace, e))
559 else:
560 raise N2VCBadArgumentsException(message='only ns_id is permitted to delete yet', bad_args=['namespace'])
561
562 self.log.info('Namespace {} deleted'.format(namespace))
563
564 async def delete_execution_environment(
565 self,
566 ee_id: str,
567 db_dict: dict = None,
568 total_timeout: float = None
569 ):
570 self.log.info('Deleting execution environment ee_id={}'.format(ee_id))
571
572 if not self._authenticated:
573 await self._juju_login()
574
575 # check arguments
576 if ee_id is None:
577 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
578
579 model_name, application_name, machine_id = self._get_ee_id_components(ee_id=ee_id)
580
581 # destroy the application
582 try:
583 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
584 except Exception as e:
585 raise N2VCException(message='Error deleting execution environment {} (application {}) : {}'
586 .format(ee_id, application_name, e))
587
588 # destroy the machine
589 # try:
590 # await self._juju_destroy_machine(
591 # model_name=model_name,
592 # machine_id=machine_id,
593 # total_timeout=total_timeout
594 # )
595 # except Exception as e:
596 # raise N2VCException(message='Error deleting execution environment {} (machine {}) : {}'
597 # .format(ee_id, machine_id, e))
598
599 self.log.info('Execution environment {} deleted'.format(ee_id))
600
601 async def exec_primitive(
602 self,
603 ee_id: str,
604 primitive_name: str,
605 params_dict: dict,
606 db_dict: dict = None,
607 progress_timeout: float = None,
608 total_timeout: float = None
609 ) -> str:
610
611 self.log.info('Executing primitive: {} on ee: {}, params: {}'.format(primitive_name, ee_id, params_dict))
612
613 if not self._authenticated:
614 await self._juju_login()
615
616 # check arguments
617 if ee_id is None or len(ee_id) == 0:
618 raise N2VCBadArgumentsException(message='ee_id is mandatory', bad_args=['ee_id'])
619 if primitive_name is None or len(primitive_name) == 0:
620 raise N2VCBadArgumentsException(message='action_name is mandatory', bad_args=['action_name'])
621 if params_dict is None:
622 params_dict = dict()
623
624 try:
625 model_name, application_name, machine_id = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
626 except Exception:
627 raise N2VCBadArgumentsException(
628 message='ee_id={} is not a valid execution environment id'.format(ee_id),
629 bad_args=['ee_id']
630 )
631
632 if primitive_name == 'config':
633 # Special case: config primitive
634 try:
635 await self._juju_configure_application(
636 model_name=model_name,
637 application_name=application_name,
638 config=params_dict,
639 db_dict=db_dict,
640 progress_timeout=progress_timeout,
641 total_timeout=total_timeout
642 )
643 except Exception as e:
644 self.log.error('Error configuring juju application: {}'.format(e))
645 raise N2VCExecutionException(
646 message='Error configuring application into ee={} : {}'.format(ee_id, e),
647 primitive_name=primitive_name
648 )
649 return 'CONFIG OK'
650 else:
651 try:
652 output, status = await self._juju_execute_action(
653 model_name=model_name,
654 application_name=application_name,
655 action_name=primitive_name,
656 db_dict=db_dict,
657 progress_timeout=progress_timeout,
658 total_timeout=total_timeout,
659 **params_dict
660 )
661 if status == 'completed':
662 return output
663 else:
664 raise Exception('status is not completed: {}'.format(status))
665 except Exception as e:
666 self.log.error('Error executing primitive {}: {}'.format(primitive_name, e))
667 raise N2VCExecutionException(
668 message='Error executing primitive {} into ee={} : {}'.format(primitive_name, ee_id, e),
669 primitive_name=primitive_name
670 )
671
672 async def disconnect(self):
673 self.log.info('closing juju N2VC...')
674 await self._juju_logout()
675
676 """
677 ##################################################################################################
678 ########################################## P R I V A T E #########################################
679 ##################################################################################################
680 """
681
682 def _write_ee_id_db(
683 self,
684 db_dict: dict,
685 ee_id: str
686 ):
687
688 # write ee_id to database: _admin.deployed.VCA.x
689 try:
690 the_table = db_dict['collection']
691 the_filter = db_dict['filter']
692 the_path = db_dict['path']
693 if not the_path[-1] == '.':
694 the_path = the_path + '.'
695 update_dict = {the_path + 'ee_id': ee_id}
696 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
697 self.db.set_one(
698 table=the_table,
699 q_filter=the_filter,
700 update_dict=update_dict,
701 fail_on_empty=True
702 )
703 except Exception as e:
704 self.log.error('Error writing ee_id to database: {}'.format(e))
705
706 @staticmethod
707 def _build_ee_id(
708 model_name: str,
709 application_name: str,
710 machine_id: str
711 ):
712 """
713 Build an execution environment id form model, application and machine
714 :param model_name:
715 :param application_name:
716 :param machine_id:
717 :return:
718 """
719 # id for the execution environment
720 return '{}.{}.{}'.format(model_name, application_name, machine_id)
721
722 @staticmethod
723 def _get_ee_id_components(
724 ee_id: str
725 ) -> (str, str, str):
726 """
727 Get model, application and machine components from an execution environment id
728 :param ee_id:
729 :return: model_name, application_name, machine_id
730 """
731
732 if ee_id is None:
733 return None, None, None
734
735 # split components of id
736 parts = ee_id.split('.')
737 model_name = parts[0]
738 application_name = parts[1]
739 machine_id = parts[2]
740 return model_name, application_name, machine_id
741
742 def _get_application_name(self, namespace: str) -> str:
743 """
744 Build application name from namespace
745 :param namespace:
746 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
747 """
748
749 # TODO: Enforce the Juju 50-character application limit
750
751 # split namespace components
752 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(namespace=namespace)
753
754 if vnf_id is None or len(vnf_id) == 0:
755 vnf_id = ''
756 else:
757 # Shorten the vnf_id to its last twelve characters
758 vnf_id = 'vnf-' + vnf_id[-12:]
759
760 if vdu_id is None or len(vdu_id) == 0:
761 vdu_id = ''
762 else:
763 # Shorten the vdu_id to its last twelve characters
764 vdu_id = '-vdu-' + vdu_id[-12:]
765
766 if vdu_count is None or len(vdu_count) == 0:
767 vdu_count = ''
768 else:
769 vdu_count = '-cnt-' + vdu_count
770
771 application_name = 'app-{}{}{}'.format(vnf_id, vdu_id, vdu_count)
772
773 return N2VCJujuConnector._format_app_name(application_name)
774
775 async def _juju_create_machine(
776 self,
777 model_name: str,
778 application_name: str,
779 machine_id: str = None,
780 db_dict: dict = None,
781 progress_timeout: float = None,
782 total_timeout: float = None
783 ) -> Machine:
784
785 self.log.debug('creating machine in model: {}, existing machine id: {}'.format(model_name, machine_id))
786
787 # get juju model and observer (create model if needed)
788 model = await self._juju_get_model(model_name=model_name)
789 observer = self.juju_observers[model_name]
790
791 # find machine id in model
792 machine = None
793 if machine_id is not None:
794 self.log.debug('Finding existing machine id {} in model'.format(machine_id))
795 # get juju existing machines in the model
796 existing_machines = await model.get_machines()
797 if machine_id in existing_machines:
798 self.log.debug('Machine id {} found in model (reusing it)'.format(machine_id))
799 machine = model.machines[machine_id]
800
801 if machine is None:
802 self.log.debug('Creating a new machine in juju...')
803 # machine does not exist, create it and wait for it
804 machine = await model.add_machine(
805 spec=None,
806 constraints=None,
807 disks=None,
808 series='xenial'
809 )
810
811 # register machine with observer
812 observer.register_machine(machine=machine, db_dict=db_dict)
813
814 # id for the execution environment
815 ee_id = N2VCJujuConnector._build_ee_id(
816 model_name=model_name,
817 application_name=application_name,
818 machine_id=str(machine.entity_id)
819 )
820
821 # write ee_id in database
822 self._write_ee_id_db(
823 db_dict=db_dict,
824 ee_id=ee_id
825 )
826
827 # wait for machine creation
828 await observer.wait_for_machine(
829 machine_id=str(machine.entity_id),
830 progress_timeout=progress_timeout,
831 total_timeout=total_timeout
832 )
833
834 else:
835
836 self.log.debug('Reusing old machine pending')
837
838 # register machine with observer
839 observer.register_machine(machine=machine, db_dict=db_dict)
840
841 # machine does exist, but it is in creation process (pending), wait for create finalisation
842 await observer.wait_for_machine(
843 machine_id=machine.entity_id,
844 progress_timeout=progress_timeout,
845 total_timeout=total_timeout)
846
847 self.log.debug("Machine ready at " + str(machine.dns_name))
848 return machine
849
850 async def _juju_provision_machine(
851 self,
852 model_name: str,
853 hostname: str,
854 username: str,
855 private_key_path: str,
856 db_dict: dict = None,
857 progress_timeout: float = None,
858 total_timeout: float = None
859 ) -> str:
860
861 if not self.api_proxy:
862 msg = 'Cannot provision machine: api_proxy is not defined'
863 self.log.error(msg=msg)
864 raise N2VCException(message=msg)
865
866 self.log.debug('provisioning machine. model: {}, hostname: {}, username: {}'.format(model_name, hostname, username))
867
868 if not self._authenticated:
869 await self._juju_login()
870
871 # get juju model and observer
872 model = await self._juju_get_model(model_name=model_name)
873 observer = self.juju_observers[model_name]
874
875 # TODO check if machine is already provisioned
876 machine_list = await model.get_machines()
877
878 provisioner = SSHProvisioner(
879 host=hostname,
880 user=username,
881 private_key_path=private_key_path,
882 log=self.log
883 )
884
885 params = None
886 try:
887 params = provisioner.provision_machine()
888 except Exception as ex:
889 msg = "Exception provisioning machine: {}".format(ex)
890 self.log.error(msg)
891 raise N2VCException(message=msg)
892
893 params.jobs = ['JobHostUnits']
894
895 connection = model.connection()
896
897 # Submit the request.
898 self.log.debug("Adding machine to model")
899 client_facade = client.ClientFacade.from_connection(connection)
900 results = await client_facade.AddMachines(params=[params])
901 error = results.machines[0].error
902 if error:
903 msg = "Error adding machine: {}}".format(error.message)
904 self.log.error(msg=msg)
905 raise ValueError(msg)
906
907 machine_id = results.machines[0].machine
908
909 # Need to run this after AddMachines has been called,
910 # as we need the machine_id
911 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
912 asyncio.ensure_future(provisioner.install_agent(
913 connection=connection,
914 nonce=params.nonce,
915 machine_id=machine_id,
916 api=self.api_proxy,
917 ))
918
919 # wait for machine in model (now, machine is not yet in model, so we must wait for it)
920 machine = None
921 for i in range(10):
922 machine_list = await model.get_machines()
923 if machine_id in machine_list:
924 self.log.debug('Machine {} found in model!'.format(machine_id))
925 machine = model.machines.get(machine_id)
926 break
927 await asyncio.sleep(2)
928
929 if machine is None:
930 msg = 'Machine {} not found in model'.format(machine_id)
931 self.log.error(msg=msg)
932 raise Exception(msg)
933
934 # register machine with observer
935 observer.register_machine(machine=machine, db_dict=db_dict)
936
937 # wait for machine creation
938 self.log.debug('waiting for provision finishes... {}'.format(machine_id))
939 await observer.wait_for_machine(
940 machine_id=machine_id,
941 progress_timeout=progress_timeout,
942 total_timeout=total_timeout
943 )
944
945 self.log.debug("Machine provisioned {}".format(machine_id))
946
947 return machine_id
948
949 async def _juju_deploy_charm(
950 self,
951 model_name: str,
952 application_name: str,
953 charm_path: str,
954 machine_id: str,
955 db_dict: dict,
956 progress_timeout: float = None,
957 total_timeout: float = None
958 ) -> (Application, int):
959
960 # get juju model and observer
961 model = await self._juju_get_model(model_name=model_name)
962 observer = self.juju_observers[model_name]
963
964 # check if application already exists
965 application = None
966 if application_name in model.applications:
967 application = model.applications[application_name]
968
969 if application is None:
970
971 # application does not exist, create it and wait for it
972 self.log.debug('deploying application {} to machine {}, model {}'
973 .format(application_name, machine_id, model_name))
974 self.log.debug('charm: {}'.format(charm_path))
975 series = 'xenial'
976 # series = None
977 application = await model.deploy(
978 entity_url=charm_path,
979 application_name=application_name,
980 channel='stable',
981 num_units=1,
982 series=series,
983 to=machine_id
984 )
985
986 # register application with observer
987 observer.register_application(application=application, db_dict=db_dict)
988
989 self.log.debug('waiting for application deployed... {}'.format(application.entity_id))
990 retries = await observer.wait_for_application(
991 application_id=application.entity_id,
992 progress_timeout=progress_timeout,
993 total_timeout=total_timeout)
994 self.log.debug('application deployed')
995
996 else:
997
998 # register application with observer
999 observer.register_application(application=application, db_dict=db_dict)
1000
1001 # application already exists, but not finalised
1002 self.log.debug('application already exists, waiting for deployed...')
1003 retries = await observer.wait_for_application(
1004 application_id=application.entity_id,
1005 progress_timeout=progress_timeout,
1006 total_timeout=total_timeout)
1007 self.log.debug('application deployed')
1008
1009 return application, retries
1010
1011 async def _juju_execute_action(
1012 self,
1013 model_name: str,
1014 application_name: str,
1015 action_name: str,
1016 db_dict: dict,
1017 progress_timeout: float = None,
1018 total_timeout: float = None,
1019 **kwargs
1020 ) -> Action:
1021
1022 # get juju model and observer
1023 model = await self._juju_get_model(model_name=model_name)
1024 observer = self.juju_observers[model_name]
1025
1026 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1027
1028 unit = None
1029 for u in application.units:
1030 if await u.is_leader_from_status():
1031 unit = u
1032 if unit is not None:
1033 actions = await application.get_actions()
1034 if action_name in actions:
1035 self.log.debug('executing action "{}" using params: {}'.format(action_name, kwargs))
1036 action = await unit.run_action(action_name, **kwargs)
1037
1038 # register action with observer
1039 observer.register_action(action=action, db_dict=db_dict)
1040
1041 await observer.wait_for_action(
1042 action_id=action.entity_id,
1043 progress_timeout=progress_timeout,
1044 total_timeout=total_timeout)
1045 self.log.debug('action completed with status: {}'.format(action.status))
1046 output = await model.get_action_output(action_uuid=action.entity_id)
1047 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1048 if action.entity_id in status:
1049 status = status[action.entity_id]
1050 else:
1051 status = 'failed'
1052 return output, status
1053
1054 raise N2VCExecutionException(
1055 message='Cannot execute action on charm',
1056 primitive_name=action_name
1057 )
1058
1059 async def _juju_configure_application(
1060 self,
1061 model_name: str,
1062 application_name: str,
1063 config: dict,
1064 db_dict: dict,
1065 progress_timeout: float = None,
1066 total_timeout: float = None
1067 ):
1068
1069 # get the application
1070 application = await self._juju_get_application(model_name=model_name, application_name=application_name)
1071
1072 self.log.debug('configuring the application {} -> {}'.format(application_name, config))
1073 res = await application.set_config(config)
1074 self.log.debug('application {} configured. res={}'.format(application_name, res))
1075
1076 # Verify the config is set
1077 new_conf = await application.get_config()
1078 for key in config:
1079 value = new_conf[key]['value']
1080 self.log.debug(' {} = {}'.format(key, value))
1081 if config[key] != value:
1082 raise N2VCException(
1083 message='key {} is not configured correctly {} != {}'.format(key, config[key], new_conf[key])
1084 )
1085
1086 # check if 'verify-ssh-credentials' action exists
1087 # unit = application.units[0]
1088 actions = await application.get_actions()
1089 if 'verify-ssh-credentials' not in actions:
1090 msg = 'Action verify-ssh-credentials does not exist in application {}'.format(application_name)
1091 self.log.debug(msg=msg)
1092 return False
1093
1094 # execute verify-credentials
1095 num_retries = 20
1096 retry_timeout = 15.0
1097 for i in range(num_retries):
1098 try:
1099 self.log.debug('Executing action verify-ssh-credentials...')
1100 output, ok = await self._juju_execute_action(
1101 model_name=model_name,
1102 application_name=application_name,
1103 action_name='verify-ssh-credentials',
1104 db_dict=db_dict,
1105 progress_timeout=progress_timeout,
1106 total_timeout=total_timeout
1107 )
1108 self.log.debug('Result: {}, output: {}'.format(ok, output))
1109 return True
1110 except Exception as e:
1111 self.log.debug('Error executing verify-ssh-credentials: {}. Retrying...'.format(e))
1112 await asyncio.sleep(retry_timeout)
1113 else:
1114 self.log.error('Error executing verify-ssh-credentials after {} retries. '.format(num_retries))
1115 return False
1116
1117 async def _juju_get_application(
1118 self,
1119 model_name: str,
1120 application_name: str
1121 ):
1122 """Get the deployed application."""
1123
1124 model = await self._juju_get_model(model_name=model_name)
1125
1126 application_name = N2VCJujuConnector._format_app_name(application_name)
1127
1128 if model.applications and application_name in model.applications:
1129 return model.applications[application_name]
1130 else:
1131 raise N2VCException(message='Cannot get application {} from model {}'.format(application_name, model_name))
1132
1133 async def _juju_get_model(self, model_name: str) -> Model:
1134 """ Get a model object from juju controller
1135 If the model does not exits, it creates it.
1136
1137 :param str model_name: name of the model
1138 :returns Model: model obtained from juju controller or Exception
1139 """
1140
1141 # format model name
1142 model_name = N2VCJujuConnector._format_model_name(model_name)
1143
1144 if model_name in self.juju_models:
1145 return self.juju_models[model_name]
1146
1147 if self._creating_model:
1148 self.log.debug('Another coroutine is creating a model. Wait...')
1149 while self._creating_model:
1150 # another coroutine is creating a model, wait
1151 await asyncio.sleep(0.1)
1152 # retry (perhaps another coroutine has created the model meanwhile)
1153 if model_name in self.juju_models:
1154 return self.juju_models[model_name]
1155
1156 try:
1157 self._creating_model = True
1158
1159 # get juju model names from juju
1160 model_list = await self.controller.list_models()
1161
1162 if model_name not in model_list:
1163 self.log.info('Model {} does not exist. Creating new model...'.format(model_name))
1164 config_dict = {'authorized-keys': self.public_key}
1165 if self.apt_mirror:
1166 config_dict['apt-mirror'] = self.apt_mirror
1167 if not self.enable_os_upgrade:
1168 config_dict['enable-os-refresh-update'] = False
1169 config_dict['enable-os-upgrade'] = False
1170
1171 model = await self.controller.add_model(
1172 model_name=model_name,
1173 config=config_dict,
1174 cloud_name=self.cloud,
1175 )
1176 self.log.info('New model created, name={}'.format(model_name))
1177 else:
1178 self.log.debug('Model already exists in juju. Getting model {}'.format(model_name))
1179 model = await self.controller.get_model(model_name)
1180 self.log.debug('Existing model in juju, name={}'.format(model_name))
1181
1182 self.juju_models[model_name] = model
1183 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1184 return model
1185
1186 except Exception as e:
1187 msg = 'Cannot get model {}. Exception: {}'.format(model_name, e)
1188 self.log.error(msg)
1189 raise N2VCException(msg)
1190 finally:
1191 self._creating_model = False
1192
1193 async def _juju_add_relation(
1194 self,
1195 model_name: str,
1196 application_name_1: str,
1197 application_name_2: str,
1198 relation_1: str,
1199 relation_2: str
1200 ):
1201
1202 # get juju model and observer
1203 model = await self._juju_get_model(model_name=model_name)
1204
1205 r1 = '{}:{}'.format(application_name_1, relation_1)
1206 r2 = '{}:{}'.format(application_name_2, relation_2)
1207
1208 self.log.debug('adding relation: {} -> {}'.format(r1, r2))
1209 try:
1210 await model.add_relation(relation1=r1, relation2=r2)
1211 except JujuAPIError as e:
1212 # If one of the applications in the relationship doesn't exist, or the relation has already been added,
1213 # let the operation fail silently.
1214 if 'not found' in e.message:
1215 return
1216 if 'already exists' in e.message:
1217 return
1218 # another execption, raise it
1219 raise e
1220
1221 async def _juju_destroy_application(
1222 self,
1223 model_name: str,
1224 application_name: str
1225 ):
1226
1227 self.log.debug('Destroying application {} in model {}'.format(application_name, model_name))
1228
1229 # get juju model and observer
1230 model = await self._juju_get_model(model_name=model_name)
1231 observer = self.juju_observers[model_name]
1232
1233 application = model.applications.get(application_name)
1234 if application:
1235 observer.unregister_application(application_name)
1236 await application.destroy()
1237 else:
1238 self.log.debug('Application not found: {}'.format(application_name))
1239
1240 async def _juju_destroy_machine(
1241 self,
1242 model_name: str,
1243 machine_id: str,
1244 total_timeout: float = None
1245 ):
1246
1247 self.log.debug('Destroying machine {} in model {}'.format(machine_id, model_name))
1248
1249 if total_timeout is None:
1250 total_timeout = 3600
1251
1252 # get juju model and observer
1253 model = await self._juju_get_model(model_name=model_name)
1254 observer = self.juju_observers[model_name]
1255
1256 machines = await model.get_machines()
1257 if machine_id in machines:
1258 machine = model.machines[machine_id]
1259 observer.unregister_machine(machine_id)
1260 # TODO: change this by machine.is_manual when this is upstreamed: https://github.com/juju/python-libjuju/pull/396
1261 if "instance-id" in machine.safe_data and machine.safe_data[
1262 "instance-id"
1263 ].startswith("manual:"):
1264 self.log.debug("machine.destroy(force=True) started.")
1265 await machine.destroy(force=True)
1266 self.log.debug("machine.destroy(force=True) passed.")
1267 # max timeout
1268 end = time.time() + total_timeout
1269 # wait for machine removal
1270 machines = await model.get_machines()
1271 while machine_id in machines and time.time() < end:
1272 self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
1273 await asyncio.sleep(0.5)
1274 machines = await model.get_machines()
1275 self.log.debug("Machine destroyed: {}".format(machine_id))
1276 else:
1277 self.log.debug('Machine not found: {}'.format(machine_id))
1278
1279 async def _juju_destroy_model(
1280 self,
1281 model_name: str,
1282 total_timeout: float = None
1283 ):
1284
1285 self.log.debug('Destroying model {}'.format(model_name))
1286
1287 if total_timeout is None:
1288 total_timeout = 3600
1289
1290 model = await self._juju_get_model(model_name=model_name)
1291
1292 if not model:
1293 raise N2VCException(
1294 message="Model {} does not exist".format(model_name)
1295 )
1296
1297 uuid = model.info.uuid
1298
1299 # destroy applications
1300 for application_name in model.applications:
1301 try:
1302 await self._juju_destroy_application(model_name=model_name, application_name=application_name)
1303 except Exception as e:
1304 self.log.error(
1305 "Error destroying application {} in model {}: {}".format(
1306 application_name,
1307 model_name,
1308 e
1309 )
1310 )
1311
1312 # destroy machines
1313 machines = await model.get_machines()
1314 for machine_id in machines:
1315 try:
1316 await self._juju_destroy_machine(model_name=model_name, machine_id=machine_id)
1317 except Exception as e:
1318 # ignore exceptions destroying machine
1319 pass
1320
1321 await self._juju_disconnect_model(model_name=model_name)
1322
1323 self.log.debug('destroying model {}...'.format(model_name))
1324 await self.controller.destroy_model(uuid)
1325 self.log.debug('model destroy requested {}'.format(model_name))
1326
1327 # wait for model is completely destroyed
1328 end = time.time() + total_timeout
1329 while time.time() < end:
1330 self.log.debug('Waiting for model is destroyed...')
1331 try:
1332 # await self.controller.get_model(uuid)
1333 models = await self.controller.list_models()
1334 if model_name not in models:
1335 self.log.debug('The model {} ({}) was destroyed'.format(model_name, uuid))
1336 return
1337 except Exception as e:
1338 pass
1339 await asyncio.sleep(1.0)
1340
1341 async def _juju_login(self):
1342 """Connect to juju controller
1343
1344 """
1345
1346 # if already authenticated, exit function
1347 if self._authenticated:
1348 return
1349
1350 # if connecting, wait for finish
1351 # another task could be trying to connect in parallel
1352 while self._connecting:
1353 await asyncio.sleep(0.1)
1354
1355 # double check after other task has finished
1356 if self._authenticated:
1357 return
1358
1359 try:
1360 self._connecting = True
1361 self.log.info(
1362 'connecting to juju controller: {} {}:{} ca_cert: {}'
1363 .format(self.url, self.username, self.secret, '\n'+self.ca_cert if self.ca_cert else 'None'))
1364
1365 # Create controller object
1366 self.controller = Controller(loop=self.loop)
1367 # Connect to controller
1368 await self.controller.connect(
1369 endpoint=self.url,
1370 username=self.username,
1371 password=self.secret,
1372 cacert=self.ca_cert
1373 )
1374 self._authenticated = True
1375 self.log.info('juju controller connected')
1376 except Exception as e:
1377 message = 'Exception connecting to juju: {}'.format(e)
1378 self.log.error(message)
1379 raise N2VCConnectionException(
1380 message=message,
1381 url=self.url
1382 )
1383 finally:
1384 self._connecting = False
1385
1386 async def _juju_logout(self):
1387 """Logout of the Juju controller."""
1388 if not self._authenticated:
1389 return False
1390
1391 # disconnect all models
1392 for model_name in self.juju_models:
1393 try:
1394 await self._juju_disconnect_model(model_name)
1395 except Exception as e:
1396 self.log.error('Error disconnecting model {} : {}'.format(model_name, e))
1397 # continue with next model...
1398
1399 self.log.info("Disconnecting controller")
1400 try:
1401 await self.controller.disconnect()
1402 except Exception as e:
1403 raise N2VCConnectionException(message='Error disconnecting controller: {}'.format(e), url=self.url)
1404
1405 self.controller = None
1406 self._authenticated = False
1407 self.log.info('disconnected')
1408
1409 async def _juju_disconnect_model(
1410 self,
1411 model_name: str
1412 ):
1413 self.log.debug("Disconnecting model {}".format(model_name))
1414 if model_name in self.juju_models:
1415 await self.juju_models[model_name].disconnect()
1416 self.juju_models[model_name] = None
1417 self.juju_observers[model_name] = None
1418 else:
1419 self.warning('Cannot disconnect model: {}'.format(model_name))
1420
1421 def _create_juju_public_key(self):
1422 """Recreate the Juju public key on lcm container, if needed
1423 Certain libjuju commands expect to be run from the same machine as Juju
1424 is bootstrapped to. This method will write the public key to disk in
1425 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1426 """
1427
1428 # Make sure that we have a public key before writing to disk
1429 if self.public_key is None or len(self.public_key) == 0:
1430 if 'OSMLCM_VCA_PUBKEY' in os.environ:
1431 self.public_key = os.getenv('OSMLCM_VCA_PUBKEY', '')
1432 if len(self.public_key) == 0:
1433 return
1434 else:
1435 return
1436
1437 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser('~'))
1438 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1439 self.log.debug('writing juju public key to file:\n{}\npublic key: {}'.format(file_path, self.public_key))
1440 if not os.path.exists(pk_path):
1441 # create path and write file
1442 os.makedirs(pk_path)
1443 with open(file_path, 'w') as f:
1444 self.log.debug('Creating juju public key file: {}'.format(file_path))
1445 f.write(self.public_key)
1446 else:
1447 self.log.debug('juju public key file already exists: {}'.format(file_path))
1448
1449 @staticmethod
1450 def _format_model_name(name: str) -> str:
1451 """Format the name of the model.
1452
1453 Model names may only contain lowercase letters, digits and hyphens
1454 """
1455
1456 return name.replace('_', '-').replace(' ', '-').lower()
1457
1458 @staticmethod
1459 def _format_app_name(name: str) -> str:
1460 """Format the name of the application (in order to assure valid application name).
1461
1462 Application names have restrictions (run juju deploy --help):
1463 - contains lowercase letters 'a'-'z'
1464 - contains numbers '0'-'9'
1465 - contains hyphens '-'
1466 - starts with a lowercase letter
1467 - not two or more consecutive hyphens
1468 - after a hyphen, not a group with all numbers
1469 """
1470
1471 def all_numbers(s: str) -> bool:
1472 for c in s:
1473 if not c.isdigit():
1474 return False
1475 return True
1476
1477 new_name = name.replace('_', '-')
1478 new_name = new_name.replace(' ', '-')
1479 new_name = new_name.lower()
1480 while new_name.find('--') >= 0:
1481 new_name = new_name.replace('--', '-')
1482 groups = new_name.split('-')
1483
1484 # find 'all numbers' groups and prefix them with a letter
1485 app_name = ''
1486 for i in range(len(groups)):
1487 group = groups[i]
1488 if all_numbers(group):
1489 group = 'z' + group
1490 if i > 0:
1491 app_name += '-'
1492 app_name += group
1493
1494 if app_name[0].isdigit():
1495 app_name = 'z' + app_name
1496
1497 return app_name