2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
31 from juju
.action
import Action
32 from juju
.application
import Application
33 from juju
.client
import client
34 from juju
.controller
import Controller
35 from juju
.errors
import JujuAPIError
36 from juju
.machine
import Machine
37 from juju
.model
import Model
38 from n2vc
.exceptions
import (
39 N2VCBadArgumentsException
,
41 N2VCConnectionException
,
42 N2VCExecutionException
,
43 N2VCInvalidCertificate
,
47 from n2vc
.juju_observer
import JujuModelObserver
48 from n2vc
.n2vc_conn
import N2VCConnector
49 from n2vc
.n2vc_conn
import obj_to_dict
, obj_to_yaml
50 from n2vc
.provisioner
import AsyncSSHProvisioner
53 class N2VCJujuConnector(N2VCConnector
):
56 ####################################################################################
57 ################################### P U B L I C ####################################
58 ####################################################################################
61 BUILT_IN_CLOUDS
= ["localhost", "microk8s"]
69 url
: str = "127.0.0.1:17070",
70 username
: str = "admin",
71 vca_config
: dict = None,
74 """Initialize juju N2VC connector
77 # parent class constructor
78 N2VCConnector
.__init
__(
86 vca_config
=vca_config
,
87 on_update_db
=on_update_db
,
90 # silence websocket traffic log
91 logging
.getLogger("websockets.protocol").setLevel(logging
.INFO
)
92 logging
.getLogger("juju.client.connection").setLevel(logging
.WARN
)
93 logging
.getLogger("model").setLevel(logging
.WARN
)
95 self
.log
.info("Initializing N2VC juju connector...")
98 ##############################################################
100 ##############################################################
105 raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
106 url_parts
= url
.split(":")
107 if len(url_parts
) != 2:
108 raise N2VCBadArgumentsException(
109 "Argument url: bad format (localhost:port) -> {}".format(url
), ["url"]
111 self
.hostname
= url_parts
[0]
113 self
.port
= int(url_parts
[1])
115 raise N2VCBadArgumentsException(
116 "url port must be a number -> {}".format(url
), ["url"]
121 raise N2VCBadArgumentsException(
122 "Argument username is mandatory", ["username"]
126 if vca_config
is None:
127 raise N2VCBadArgumentsException(
128 "Argument vca_config is mandatory", ["vca_config"]
131 if "secret" in vca_config
:
132 self
.secret
= vca_config
["secret"]
134 raise N2VCBadArgumentsException(
135 "Argument vca_config.secret is mandatory", ["vca_config.secret"]
138 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
139 # if exists, it will be written in lcm container: _create_juju_public_key()
140 if "public_key" in vca_config
:
141 self
.public_key
= vca_config
["public_key"]
143 self
.public_key
= None
145 # TODO: Verify ca_cert is valid before using. VCA will crash
146 # if the ca_cert isn't formatted correctly.
147 def base64_to_cacert(b64string
):
148 """Convert the base64-encoded string containing the VCA CACERT.
154 cacert
= base64
.b64decode(b64string
).decode("utf-8")
156 cacert
= re
.sub(r
"\\n", r
"\n", cacert
,)
157 except binascii
.Error
as e
:
158 self
.log
.debug("Caught binascii.Error: {}".format(e
))
159 raise N2VCInvalidCertificate(message
="Invalid CA Certificate")
163 self
.ca_cert
= vca_config
.get("ca_cert")
165 self
.ca_cert
= base64_to_cacert(vca_config
["ca_cert"])
167 if "api_proxy" in vca_config
:
168 self
.api_proxy
= vca_config
["api_proxy"]
170 "api_proxy for native charms configured: {}".format(self
.api_proxy
)
174 "api_proxy is not configured. Support for native charms is disabled"
177 if "enable_os_upgrade" in vca_config
:
178 self
.enable_os_upgrade
= vca_config
["enable_os_upgrade"]
180 self
.enable_os_upgrade
= True
182 if "apt_mirror" in vca_config
:
183 self
.apt_mirror
= vca_config
["apt_mirror"]
185 self
.apt_mirror
= None
187 self
.cloud
= vca_config
.get("cloud")
188 # self.log.debug('Arguments have been checked')
191 self
.controller
= None # it will be filled when connect to juju
192 self
.juju_models
= {} # model objects for every model_name
193 self
.juju_observers
= {} # model observers for every model_name
195 False # while connecting to juju (to avoid duplicate connections)
197 self
._authenticated
= (
198 False # it will be True when juju connection be stablished
200 self
._creating
_model
= False # True during model creation
202 # create juju pub key file in lcm container at
203 # ./local/share/juju/ssh/juju_id_rsa.pub
204 self
._create
_juju
_public
_key
()
206 self
.log
.info("N2VC juju connector initialized")
208 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
210 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
212 if not self
._authenticated
:
213 await self
._juju
_login
()
215 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
218 # model name is ns_id
220 if model_name
is None:
221 msg
= "Namespace {} not valid".format(namespace
)
223 raise N2VCBadArgumentsException(msg
, ["namespace"])
225 # get juju model (create model if needed)
226 model
= await self
._juju
_get
_model
(model_name
=model_name
)
228 status
= await model
.get_status()
231 return obj_to_yaml(status
)
233 return obj_to_dict(status
)
235 async def create_execution_environment(
239 reuse_ee_id
: str = None,
240 progress_timeout
: float = None,
241 total_timeout
: float = None,
245 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
246 namespace
, reuse_ee_id
250 if not self
._authenticated
:
251 await self
._juju
_login
()
255 model_name
, application_name
, machine_id
= self
._get
_ee
_id
_components
(
265 ) = self
._get
_namespace
_components
(namespace
=namespace
)
266 # model name is ns_id
269 application_name
= self
._get
_application
_name
(namespace
=namespace
)
272 "model name: {}, application name: {}, machine_id: {}".format(
273 model_name
, application_name
, machine_id
277 # create or reuse a new juju machine
279 machine
= await self
._juju
_create
_machine
(
280 model_name
=model_name
,
281 application_name
=application_name
,
282 machine_id
=machine_id
,
284 progress_timeout
=progress_timeout
,
285 total_timeout
=total_timeout
,
287 except Exception as e
:
288 message
= "Error creating machine on juju: {}".format(e
)
289 self
.log
.error(message
)
290 raise N2VCException(message
=message
)
292 # id for the execution environment
293 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
294 model_name
=model_name
,
295 application_name
=application_name
,
296 machine_id
=str(machine
.entity_id
),
298 self
.log
.debug("ee_id: {}".format(ee_id
))
300 # new machine credentials
302 credentials
["hostname"] = machine
.dns_name
305 "Execution environment created. ee_id: {}, credentials: {}".format(
310 return ee_id
, credentials
312 async def register_execution_environment(
317 progress_timeout
: float = None,
318 total_timeout
: float = None,
321 if not self
._authenticated
:
322 await self
._juju
_login
()
325 "Registering execution environment. namespace={}, credentials={}".format(
326 namespace
, credentials
330 if credentials
is None:
331 raise N2VCBadArgumentsException(
332 message
="credentials are mandatory", bad_args
=["credentials"]
334 if credentials
.get("hostname"):
335 hostname
= credentials
["hostname"]
337 raise N2VCBadArgumentsException(
338 message
="hostname is mandatory", bad_args
=["credentials.hostname"]
340 if credentials
.get("username"):
341 username
= credentials
["username"]
343 raise N2VCBadArgumentsException(
344 message
="username is mandatory", bad_args
=["credentials.username"]
346 if "private_key_path" in credentials
:
347 private_key_path
= credentials
["private_key_path"]
349 # if not passed as argument, use generated private key path
350 private_key_path
= self
.private_key_path
352 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
359 application_name
= self
._get
_application
_name
(namespace
=namespace
)
361 # register machine on juju
363 machine_id
= await self
._juju
_provision
_machine
(
364 model_name
=model_name
,
367 private_key_path
=private_key_path
,
369 progress_timeout
=progress_timeout
,
370 total_timeout
=total_timeout
,
372 except Exception as e
:
373 self
.log
.error("Error registering machine: {}".format(e
))
375 message
="Error registering machine on juju: {}".format(e
)
378 self
.log
.info("Machine registered: {}".format(machine_id
))
380 # id for the execution environment
381 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
382 model_name
=model_name
,
383 application_name
=application_name
,
384 machine_id
=str(machine_id
),
387 self
.log
.info("Execution environment registered. ee_id: {}".format(ee_id
))
391 async def install_configuration_sw(
396 progress_timeout
: float = None,
397 total_timeout
: float = None,
403 "Installing configuration sw on ee_id: {}, "
404 "artifact path: {}, db_dict: {}"
405 ).format(ee_id
, artifact_path
, db_dict
)
408 if not self
._authenticated
:
409 await self
._juju
_login
()
412 if ee_id
is None or len(ee_id
) == 0:
413 raise N2VCBadArgumentsException(
414 message
="ee_id is mandatory", bad_args
=["ee_id"]
416 if artifact_path
is None or len(artifact_path
) == 0:
417 raise N2VCBadArgumentsException(
418 message
="artifact_path is mandatory", bad_args
=["artifact_path"]
421 raise N2VCBadArgumentsException(
422 message
="db_dict is mandatory", bad_args
=["db_dict"]
430 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
432 "model: {}, application: {}, machine: {}".format(
433 model_name
, application_name
, machine_id
437 raise N2VCBadArgumentsException(
438 message
="ee_id={} is not a valid execution environment id".format(
444 # remove // in charm path
445 while artifact_path
.find("//") >= 0:
446 artifact_path
= artifact_path
.replace("//", "/")
449 if not self
.fs
.file_exists(artifact_path
, mode
="dir"):
450 msg
= "artifact path does not exist: {}".format(artifact_path
)
451 raise N2VCBadArgumentsException(message
=msg
, bad_args
=["artifact_path"])
453 if artifact_path
.startswith("/"):
454 full_path
= self
.fs
.path
+ artifact_path
456 full_path
= self
.fs
.path
+ "/" + artifact_path
459 await self
._juju
_deploy
_charm
(
460 model_name
=model_name
,
461 application_name
=application_name
,
462 charm_path
=full_path
,
463 machine_id
=machine_id
,
465 progress_timeout
=progress_timeout
,
466 total_timeout
=total_timeout
,
469 except Exception as e
:
471 message
="Error desploying charm into ee={} : {}".format(ee_id
, e
)
474 self
.log
.info("Configuration sw installed")
476 async def get_ee_ssh_public__key(
480 progress_timeout
: float = None,
481 total_timeout
: float = None,
486 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
487 ).format(ee_id
, db_dict
)
490 if not self
._authenticated
:
491 await self
._juju
_login
()
494 if ee_id
is None or len(ee_id
) == 0:
495 raise N2VCBadArgumentsException(
496 message
="ee_id is mandatory", bad_args
=["ee_id"]
499 raise N2VCBadArgumentsException(
500 message
="db_dict is mandatory", bad_args
=["db_dict"]
508 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
510 "model: {}, application: {}, machine: {}".format(
511 model_name
, application_name
, machine_id
515 raise N2VCBadArgumentsException(
516 message
="ee_id={} is not a valid execution environment id".format(
522 # try to execute ssh layer primitives (if exist):
528 # execute action: generate-ssh-key
530 output
, _status
= await self
._juju
_execute
_action
(
531 model_name
=model_name
,
532 application_name
=application_name
,
533 action_name
="generate-ssh-key",
535 progress_timeout
=progress_timeout
,
536 total_timeout
=total_timeout
,
538 except Exception as e
:
540 "Skipping exception while executing action generate-ssh-key: {}".format(
545 # execute action: get-ssh-public-key
547 output
, _status
= await self
._juju
_execute
_action
(
548 model_name
=model_name
,
549 application_name
=application_name
,
550 action_name
="get-ssh-public-key",
552 progress_timeout
=progress_timeout
,
553 total_timeout
=total_timeout
,
555 except Exception as e
:
556 msg
= "Cannot execute action get-ssh-public-key: {}\n".format(e
)
558 raise N2VCException(msg
)
560 # return public key if exists
561 return output
["pubkey"] if "pubkey" in output
else output
563 async def add_relation(
564 self
, ee_id_1
: str, ee_id_2
: str, endpoint_1
: str, endpoint_2
: str
568 "adding new relation between {} and {}, endpoints: {}, {}".format(
569 ee_id_1
, ee_id_2
, endpoint_1
, endpoint_2
575 message
= "EE 1 is mandatory"
576 self
.log
.error(message
)
577 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_1"])
579 message
= "EE 2 is mandatory"
580 self
.log
.error(message
)
581 raise N2VCBadArgumentsException(message
=message
, bad_args
=["ee_id_2"])
583 message
= "endpoint 1 is mandatory"
584 self
.log
.error(message
)
585 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_1"])
587 message
= "endpoint 2 is mandatory"
588 self
.log
.error(message
)
589 raise N2VCBadArgumentsException(message
=message
, bad_args
=["endpoint_2"])
591 if not self
._authenticated
:
592 await self
._juju
_login
()
594 # get the model, the applications and the machines from the ee_id's
595 model_1
, app_1
, _machine_1
= self
._get
_ee
_id
_components
(ee_id_1
)
596 model_2
, app_2
, _machine_2
= self
._get
_ee
_id
_components
(ee_id_2
)
598 # model must be the same
599 if model_1
!= model_2
:
600 message
= "EE models are not the same: {} vs {}".format(ee_id_1
, ee_id_2
)
601 self
.log
.error(message
)
602 raise N2VCBadArgumentsException(
603 message
=message
, bad_args
=["ee_id_1", "ee_id_2"]
606 # add juju relations between two applications
608 await self
._juju
_add
_relation
(
610 application_name_1
=app_1
,
611 application_name_2
=app_2
,
612 relation_1
=endpoint_1
,
613 relation_2
=endpoint_2
,
615 except Exception as e
:
616 message
= "Error adding relation between {} and {}: {}".format(
619 self
.log
.error(message
)
620 raise N2VCException(message
=message
)
622 async def remove_relation(self
):
623 if not self
._authenticated
:
624 await self
._juju
_login
()
626 self
.log
.info("Method not implemented yet")
627 raise MethodNotImplemented()
629 async def deregister_execution_environments(self
):
630 if not self
._authenticated
:
631 await self
._juju
_login
()
633 self
.log
.info("Method not implemented yet")
634 raise MethodNotImplemented()
636 async def delete_namespace(
637 self
, namespace
: str, db_dict
: dict = None, total_timeout
: float = None
639 self
.log
.info("Deleting namespace={}".format(namespace
))
641 if not self
._authenticated
:
642 await self
._juju
_login
()
645 if namespace
is None:
646 raise N2VCBadArgumentsException(
647 message
="namespace is mandatory", bad_args
=["namespace"]
650 _nsi_id
, ns_id
, _vnf_id
, _vdu_id
, _vdu_count
= self
._get
_namespace
_components
(
653 if ns_id
is not None:
655 await self
._juju
_destroy
_model
(
656 model_name
=ns_id
, total_timeout
=total_timeout
660 except Exception as e
:
662 message
="Error deleting namespace {} : {}".format(namespace
, e
)
665 raise N2VCBadArgumentsException(
666 message
="only ns_id is permitted to delete yet", bad_args
=["namespace"]
669 self
.log
.info("Namespace {} deleted".format(namespace
))
671 async def delete_execution_environment(
672 self
, ee_id
: str, db_dict
: dict = None, total_timeout
: float = None
674 self
.log
.info("Deleting execution environment ee_id={}".format(ee_id
))
676 if not self
._authenticated
:
677 await self
._juju
_login
()
681 raise N2VCBadArgumentsException(
682 message
="ee_id is mandatory", bad_args
=["ee_id"]
685 model_name
, application_name
, _machine_id
= self
._get
_ee
_id
_components
(
689 # destroy the application
691 await self
._juju
_destroy
_application
(
692 model_name
=model_name
, application_name
=application_name
694 except Exception as e
:
697 "Error deleting execution environment {} (application {}) : {}"
698 ).format(ee_id
, application_name
, e
)
701 # destroy the machine
703 # await self._juju_destroy_machine(
704 # model_name=model_name,
705 # machine_id=machine_id,
706 # total_timeout=total_timeout
708 # except Exception as e:
709 # raise N2VCException(
710 # message='Error deleting execution environment {} (machine {}) : {}'
711 # .format(ee_id, machine_id, e))
713 self
.log
.info("Execution environment {} deleted".format(ee_id
))
715 async def exec_primitive(
720 db_dict
: dict = None,
721 progress_timeout
: float = None,
722 total_timeout
: float = None,
726 "Executing primitive: {} on ee: {}, params: {}".format(
727 primitive_name
, ee_id
, params_dict
731 if not self
._authenticated
:
732 await self
._juju
_login
()
735 if ee_id
is None or len(ee_id
) == 0:
736 raise N2VCBadArgumentsException(
737 message
="ee_id is mandatory", bad_args
=["ee_id"]
739 if primitive_name
is None or len(primitive_name
) == 0:
740 raise N2VCBadArgumentsException(
741 message
="action_name is mandatory", bad_args
=["action_name"]
743 if params_dict
is None:
751 ) = N2VCJujuConnector
._get
_ee
_id
_components
(ee_id
=ee_id
)
753 raise N2VCBadArgumentsException(
754 message
="ee_id={} is not a valid execution environment id".format(
760 if primitive_name
== "config":
761 # Special case: config primitive
763 await self
._juju
_configure
_application
(
764 model_name
=model_name
,
765 application_name
=application_name
,
768 progress_timeout
=progress_timeout
,
769 total_timeout
=total_timeout
,
771 except Exception as e
:
772 self
.log
.error("Error configuring juju application: {}".format(e
))
773 raise N2VCExecutionException(
774 message
="Error configuring application into ee={} : {}".format(
777 primitive_name
=primitive_name
,
782 output
, status
= await self
._juju
_execute
_action
(
783 model_name
=model_name
,
784 application_name
=application_name
,
785 action_name
=primitive_name
,
787 progress_timeout
=progress_timeout
,
788 total_timeout
=total_timeout
,
791 if status
== "completed":
794 raise Exception("status is not completed: {}".format(status
))
795 except Exception as e
:
797 "Error executing primitive {}: {}".format(primitive_name
, e
)
799 raise N2VCExecutionException(
800 message
="Error executing primitive {} into ee={} : {}".format(
801 primitive_name
, ee_id
, e
803 primitive_name
=primitive_name
,
806 async def disconnect(self
):
807 self
.log
.info("closing juju N2VC...")
808 await self
._juju
_logout
()
811 ####################################################################################
812 ################################### P R I V A T E ##################################
813 ####################################################################################
816 def _write_ee_id_db(self
, db_dict
: dict, ee_id
: str):
818 # write ee_id to database: _admin.deployed.VCA.x
820 the_table
= db_dict
["collection"]
821 the_filter
= db_dict
["filter"]
822 the_path
= db_dict
["path"]
823 if not the_path
[-1] == ".":
824 the_path
= the_path
+ "."
825 update_dict
= {the_path
+ "ee_id": ee_id
}
826 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
830 update_dict
=update_dict
,
833 except asyncio
.CancelledError
:
835 except Exception as e
:
836 self
.log
.error("Error writing ee_id to database: {}".format(e
))
839 def _build_ee_id(model_name
: str, application_name
: str, machine_id
: str):
841 Build an execution environment id form model, application and machine
843 :param application_name:
847 # id for the execution environment
848 return "{}.{}.{}".format(model_name
, application_name
, machine_id
)
851 def _get_ee_id_components(ee_id
: str) -> (str, str, str):
853 Get model, application and machine components from an execution environment id
855 :return: model_name, application_name, machine_id
859 return None, None, None
861 # split components of id
862 parts
= ee_id
.split(".")
863 model_name
= parts
[0]
864 application_name
= parts
[1]
865 machine_id
= parts
[2]
866 return model_name
, application_name
, machine_id
868 def _get_application_name(self
, namespace
: str) -> str:
870 Build application name from namespace
872 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
875 # TODO: Enforce the Juju 50-character application limit
877 # split namespace components
878 _
, _
, vnf_id
, vdu_id
, vdu_count
= self
._get
_namespace
_components
(
882 if vnf_id
is None or len(vnf_id
) == 0:
885 # Shorten the vnf_id to its last twelve characters
886 vnf_id
= "vnf-" + vnf_id
[-12:]
888 if vdu_id
is None or len(vdu_id
) == 0:
891 # Shorten the vdu_id to its last twelve characters
892 vdu_id
= "-vdu-" + vdu_id
[-12:]
894 if vdu_count
is None or len(vdu_count
) == 0:
897 vdu_count
= "-cnt-" + vdu_count
899 application_name
= "app-{}{}{}".format(vnf_id
, vdu_id
, vdu_count
)
901 return N2VCJujuConnector
._format
_app
_name
(application_name
)
903 async def _juju_create_machine(
906 application_name
: str,
907 machine_id
: str = None,
908 db_dict
: dict = None,
909 progress_timeout
: float = None,
910 total_timeout
: float = None,
914 "creating machine in model: {}, existing machine id: {}".format(
915 model_name
, machine_id
919 # get juju model and observer (create model if needed)
920 model
= await self
._juju
_get
_model
(model_name
=model_name
)
921 observer
= self
.juju_observers
[model_name
]
923 # find machine id in model
925 if machine_id
is not None:
926 self
.log
.debug("Finding existing machine id {} in model".format(machine_id
))
927 # get juju existing machines in the model
928 existing_machines
= await model
.get_machines()
929 if machine_id
in existing_machines
:
931 "Machine id {} found in model (reusing it)".format(machine_id
)
933 machine
= model
.machines
[machine_id
]
936 self
.log
.debug("Creating a new machine in juju...")
937 # machine does not exist, create it and wait for it
938 machine
= await model
.add_machine(
939 spec
=None, constraints
=None, disks
=None, series
="xenial"
942 # register machine with observer
943 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
945 # id for the execution environment
946 ee_id
= N2VCJujuConnector
._build
_ee
_id
(
947 model_name
=model_name
,
948 application_name
=application_name
,
949 machine_id
=str(machine
.entity_id
),
952 # write ee_id in database
953 self
._write
_ee
_id
_db
(db_dict
=db_dict
, ee_id
=ee_id
)
955 # wait for machine creation
956 await observer
.wait_for_machine(
957 machine_id
=str(machine
.entity_id
),
958 progress_timeout
=progress_timeout
,
959 total_timeout
=total_timeout
,
964 self
.log
.debug("Reusing old machine pending")
966 # register machine with observer
967 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
969 # machine does exist, but it is in creation process (pending), wait for
970 # create finalisation
971 await observer
.wait_for_machine(
972 machine_id
=machine
.entity_id
,
973 progress_timeout
=progress_timeout
,
974 total_timeout
=total_timeout
,
977 self
.log
.debug("Machine ready at " + str(machine
.dns_name
))
980 async def _juju_provision_machine(
985 private_key_path
: str,
986 db_dict
: dict = None,
987 progress_timeout
: float = None,
988 total_timeout
: float = None,
991 if not self
.api_proxy
:
992 msg
= "Cannot provision machine: api_proxy is not defined"
993 self
.log
.error(msg
=msg
)
994 raise N2VCException(message
=msg
)
997 "provisioning machine. model: {}, hostname: {}, username: {}".format(
998 model_name
, hostname
, username
1002 if not self
._authenticated
:
1003 await self
._juju
_login
()
1005 # get juju model and observer
1006 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1007 observer
= self
.juju_observers
[model_name
]
1009 # TODO check if machine is already provisioned
1010 machine_list
= await model
.get_machines()
1012 provisioner
= AsyncSSHProvisioner(
1015 private_key_path
=private_key_path
,
1021 params
= await provisioner
.provision_machine()
1022 except Exception as ex
:
1023 msg
= "Exception provisioning machine: {}".format(ex
)
1025 raise N2VCException(message
=msg
)
1027 params
.jobs
= ["JobHostUnits"]
1029 connection
= model
.connection()
1031 # Submit the request.
1032 self
.log
.debug("Adding machine to model")
1033 client_facade
= client
.ClientFacade
.from_connection(connection
)
1034 results
= await client_facade
.AddMachines(params
=[params
])
1035 error
= results
.machines
[0].error
1037 msg
= "Error adding machine: {}}".format(error
.message
)
1038 self
.log
.error(msg
=msg
)
1039 raise ValueError(msg
)
1041 machine_id
= results
.machines
[0].machine
1043 # Need to run this after AddMachines has been called,
1044 # as we need the machine_id
1045 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
1046 asyncio
.ensure_future(
1047 provisioner
.install_agent(
1048 connection
=connection
,
1050 machine_id
=machine_id
,
1055 # wait for machine in model (now, machine is not yet in model, so we must
1059 machine_list
= await model
.get_machines()
1060 if machine_id
in machine_list
:
1061 self
.log
.debug("Machine {} found in model!".format(machine_id
))
1062 machine
= model
.machines
.get(machine_id
)
1064 await asyncio
.sleep(2)
1067 msg
= "Machine {} not found in model".format(machine_id
)
1068 self
.log
.error(msg
=msg
)
1069 raise Exception(msg
)
1071 # register machine with observer
1072 observer
.register_machine(machine
=machine
, db_dict
=db_dict
)
1074 # wait for machine creation
1075 self
.log
.debug("waiting for provision finishes... {}".format(machine_id
))
1076 await observer
.wait_for_machine(
1077 machine_id
=machine_id
,
1078 progress_timeout
=progress_timeout
,
1079 total_timeout
=total_timeout
,
1082 self
.log
.debug("Machine provisioned {}".format(machine_id
))
1086 async def _juju_deploy_charm(
1089 application_name
: str,
1093 progress_timeout
: float = None,
1094 total_timeout
: float = None,
1095 config
: dict = None,
1096 ) -> (Application
, int):
1098 # get juju model and observer
1099 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1100 observer
= self
.juju_observers
[model_name
]
1102 # check if application already exists
1104 if application_name
in model
.applications
:
1105 application
= model
.applications
[application_name
]
1107 if application
is None:
1109 # application does not exist, create it and wait for it
1111 "deploying application {} to machine {}, model {}".format(
1112 application_name
, machine_id
, model_name
1115 self
.log
.debug("charm: {}".format(charm_path
))
1118 application
= await model
.deploy(
1119 entity_url
=charm_path
,
1120 application_name
=application_name
,
1128 # register application with observer
1129 observer
.register_application(application
=application
, db_dict
=db_dict
)
1132 "waiting for application deployed... {}".format(application
.entity_id
)
1134 retries
= await observer
.wait_for_application(
1135 application_id
=application
.entity_id
,
1136 progress_timeout
=progress_timeout
,
1137 total_timeout
=total_timeout
,
1139 self
.log
.debug("application deployed")
1143 # register application with observer
1144 observer
.register_application(application
=application
, db_dict
=db_dict
)
1146 # application already exists, but not finalised
1147 self
.log
.debug("application already exists, waiting for deployed...")
1148 retries
= await observer
.wait_for_application(
1149 application_id
=application
.entity_id
,
1150 progress_timeout
=progress_timeout
,
1151 total_timeout
=total_timeout
,
1153 self
.log
.debug("application deployed")
1155 return application
, retries
1157 async def _juju_execute_action(
1160 application_name
: str,
1163 progress_timeout
: float = None,
1164 total_timeout
: float = None,
1168 # get juju model and observer
1169 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1170 observer
= self
.juju_observers
[model_name
]
1172 application
= await self
._juju
_get
_application
(
1173 model_name
=model_name
, application_name
=application_name
1177 for u
in application
.units
:
1178 if await u
.is_leader_from_status():
1180 if unit
is not None:
1181 actions
= await application
.get_actions()
1182 if action_name
in actions
:
1184 'executing action "{}" using params: {}'.format(action_name
, kwargs
)
1186 action
= await unit
.run_action(action_name
, **kwargs
)
1188 # register action with observer
1189 observer
.register_action(action
=action
, db_dict
=db_dict
)
1191 await observer
.wait_for_action(
1192 action_id
=action
.entity_id
,
1193 progress_timeout
=progress_timeout
,
1194 total_timeout
=total_timeout
,
1196 self
.log
.debug("action completed with status: {}".format(action
.status
))
1197 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
1198 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
1199 if action
.entity_id
in status
:
1200 status
= status
[action
.entity_id
]
1203 return output
, status
1205 raise N2VCExecutionException(
1206 message
="Cannot execute action on charm", primitive_name
=action_name
1209 async def _juju_configure_application(
1212 application_name
: str,
1215 progress_timeout
: float = None,
1216 total_timeout
: float = None,
1219 # get the application
1220 application
= await self
._juju
_get
_application
(
1221 model_name
=model_name
, application_name
=application_name
1225 "configuring the application {} -> {}".format(application_name
, config
)
1227 res
= await application
.set_config(config
)
1229 "application {} configured. res={}".format(application_name
, res
)
1232 # Verify the config is set
1233 new_conf
= await application
.get_config()
1235 value
= new_conf
[key
]["value"]
1236 self
.log
.debug(" {} = {}".format(key
, value
))
1237 if config
[key
] != value
:
1238 raise N2VCException(
1239 message
="key {} is not configured correctly {} != {}".format(
1240 key
, config
[key
], new_conf
[key
]
1244 # check if 'verify-ssh-credentials' action exists
1245 # unit = application.units[0]
1246 actions
= await application
.get_actions()
1247 if "verify-ssh-credentials" not in actions
:
1249 "Action verify-ssh-credentials does not exist in application {}"
1250 ).format(application_name
)
1251 self
.log
.debug(msg
=msg
)
1254 # execute verify-credentials
1256 retry_timeout
= 15.0
1257 for _
in range(num_retries
):
1259 self
.log
.debug("Executing action verify-ssh-credentials...")
1260 output
, ok
= await self
._juju
_execute
_action
(
1261 model_name
=model_name
,
1262 application_name
=application_name
,
1263 action_name
="verify-ssh-credentials",
1265 progress_timeout
=progress_timeout
,
1266 total_timeout
=total_timeout
,
1268 self
.log
.debug("Result: {}, output: {}".format(ok
, output
))
1270 except asyncio
.CancelledError
:
1272 except Exception as e
:
1274 "Error executing verify-ssh-credentials: {}. Retrying...".format(e
)
1276 await asyncio
.sleep(retry_timeout
)
1279 "Error executing verify-ssh-credentials after {} retries. ".format(
1285 async def _juju_get_application(self
, model_name
: str, application_name
: str):
1286 """Get the deployed application."""
1288 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1290 application_name
= N2VCJujuConnector
._format
_app
_name
(application_name
)
1292 if model
.applications
and application_name
in model
.applications
:
1293 return model
.applications
[application_name
]
1295 raise N2VCException(
1296 message
="Cannot get application {} from model {}".format(
1297 application_name
, model_name
1301 async def _juju_get_model(self
, model_name
: str) -> Model
:
1302 """ Get a model object from juju controller
1303 If the model does not exits, it creates it.
1305 :param str model_name: name of the model
1306 :returns Model: model obtained from juju controller or Exception
1310 model_name
= N2VCJujuConnector
._format
_model
_name
(model_name
)
1312 if model_name
in self
.juju_models
:
1313 return self
.juju_models
[model_name
]
1315 if self
._creating
_model
:
1316 self
.log
.debug("Another coroutine is creating a model. Wait...")
1317 while self
._creating
_model
:
1318 # another coroutine is creating a model, wait
1319 await asyncio
.sleep(0.1)
1320 # retry (perhaps another coroutine has created the model meanwhile)
1321 if model_name
in self
.juju_models
:
1322 return self
.juju_models
[model_name
]
1325 self
._creating
_model
= True
1327 # get juju model names from juju
1328 model_list
= await self
.controller
.list_models()
1330 if model_name
not in model_list
:
1332 "Model {} does not exist. Creating new model...".format(model_name
)
1334 config_dict
= {"authorized-keys": self
.public_key
}
1336 config_dict
["apt-mirror"] = self
.apt_mirror
1337 if not self
.enable_os_upgrade
:
1338 config_dict
["enable-os-refresh-update"] = False
1339 config_dict
["enable-os-upgrade"] = False
1340 if self
.cloud
in self
.BUILT_IN_CLOUDS
:
1341 model
= await self
.controller
.add_model(
1342 model_name
=model_name
,
1344 cloud_name
=self
.cloud
,
1347 model
= await self
.controller
.add_model(
1348 model_name
=model_name
,
1350 cloud_name
=self
.cloud
,
1351 credential_name
=self
.cloud
,
1353 self
.log
.info("New model created, name={}".format(model_name
))
1356 "Model already exists in juju. Getting model {}".format(model_name
)
1358 model
= await self
.controller
.get_model(model_name
)
1359 self
.log
.debug("Existing model in juju, name={}".format(model_name
))
1361 self
.juju_models
[model_name
] = model
1362 self
.juju_observers
[model_name
] = JujuModelObserver(n2vc
=self
, model
=model
)
1365 except Exception as e
:
1366 msg
= "Cannot get model {}. Exception: {}".format(model_name
, e
)
1368 raise N2VCException(msg
)
1370 self
._creating
_model
= False
1372 async def _juju_add_relation(
1375 application_name_1
: str,
1376 application_name_2
: str,
1381 # get juju model and observer
1382 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1384 r1
= "{}:{}".format(application_name_1
, relation_1
)
1385 r2
= "{}:{}".format(application_name_2
, relation_2
)
1387 self
.log
.debug("adding relation: {} -> {}".format(r1
, r2
))
1389 await model
.add_relation(relation1
=r1
, relation2
=r2
)
1390 except JujuAPIError
as e
:
1391 # If one of the applications in the relationship doesn't exist, or the
1392 # relation has already been added,
1393 # let the operation fail silently.
1394 if "not found" in e
.message
:
1396 if "already exists" in e
.message
:
1398 # another execption, raise it
1401 async def _juju_destroy_application(self
, model_name
: str, application_name
: str):
1404 "Destroying application {} in model {}".format(application_name
, model_name
)
1407 # get juju model and observer
1408 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1409 observer
= self
.juju_observers
[model_name
]
1411 application
= model
.applications
.get(application_name
)
1413 observer
.unregister_application(application_name
)
1414 await application
.destroy()
1416 self
.log
.debug("Application not found: {}".format(application_name
))
1418 async def _juju_destroy_machine(
1419 self
, model_name
: str, machine_id
: str, total_timeout
: float = None
1423 "Destroying machine {} in model {}".format(machine_id
, model_name
)
1426 if total_timeout
is None:
1427 total_timeout
= 3600
1429 # get juju model and observer
1430 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1431 observer
= self
.juju_observers
[model_name
]
1433 machines
= await model
.get_machines()
1434 if machine_id
in machines
:
1435 machine
= model
.machines
[machine_id
]
1436 observer
.unregister_machine(machine_id
)
1437 # TODO: change this by machine.is_manual when this is upstreamed:
1438 # https://github.com/juju/python-libjuju/pull/396
1439 if "instance-id" in machine
.safe_data
and machine
.safe_data
[
1441 ].startswith("manual:"):
1442 self
.log
.debug("machine.destroy(force=True) started.")
1443 await machine
.destroy(force
=True)
1444 self
.log
.debug("machine.destroy(force=True) passed.")
1446 end
= time
.time() + total_timeout
1447 # wait for machine removal
1448 machines
= await model
.get_machines()
1449 while machine_id
in machines
and time
.time() < end
:
1451 "Waiting for machine {} is destroyed".format(machine_id
)
1453 await asyncio
.sleep(0.5)
1454 machines
= await model
.get_machines()
1455 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
1457 self
.log
.debug("Machine not found: {}".format(machine_id
))
1459 async def _juju_destroy_model(self
, model_name
: str, total_timeout
: float = None):
1461 self
.log
.debug("Destroying model {}".format(model_name
))
1463 if total_timeout
is None:
1464 total_timeout
= 3600
1465 end
= time
.time() + total_timeout
1467 model
= await self
._juju
_get
_model
(model_name
=model_name
)
1470 raise N2VCNotFound(message
="Model {} does not exist".format(model_name
))
1472 uuid
= model
.info
.uuid
1474 # destroy applications
1475 for application_name
in model
.applications
:
1477 await self
._juju
_destroy
_application
(
1478 model_name
=model_name
, application_name
=application_name
1480 except Exception as e
:
1482 "Error destroying application {} in model {}: {}".format(
1483 application_name
, model_name
, e
1488 machines
= await model
.get_machines()
1489 for machine_id
in machines
:
1491 await self
._juju
_destroy
_machine
(
1492 model_name
=model_name
, machine_id
=machine_id
1494 except asyncio
.CancelledError
:
1497 # ignore exceptions destroying machine
1500 await self
._juju
_disconnect
_model
(model_name
=model_name
)
1502 self
.log
.debug("destroying model {}...".format(model_name
))
1503 await self
.controller
.destroy_model(uuid
)
1504 # self.log.debug('model destroy requested {}'.format(model_name))
1506 # wait for model is completely destroyed
1507 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
1509 while time
.time() < end
:
1511 # await self.controller.get_model(uuid)
1512 models
= await self
.controller
.list_models()
1513 if model_name
not in models
:
1515 "The model {} ({}) was destroyed".format(model_name
, uuid
)
1518 except asyncio
.CancelledError
:
1520 except Exception as e
:
1522 await asyncio
.sleep(5)
1523 raise N2VCException(
1524 "Timeout waiting for model {} to be destroyed {}".format(
1525 model_name
, last_exception
1529 async def _juju_login(self
):
1530 """Connect to juju controller
1534 # if already authenticated, exit function
1535 if self
._authenticated
:
1538 # if connecting, wait for finish
1539 # another task could be trying to connect in parallel
1540 while self
._connecting
:
1541 await asyncio
.sleep(0.1)
1543 # double check after other task has finished
1544 if self
._authenticated
:
1548 self
._connecting
= True
1550 "connecting to juju controller: {} {}:{}{}".format(
1553 self
.secret
[:8] + "...",
1554 " with ca_cert" if self
.ca_cert
else "",
1558 # Create controller object
1559 self
.controller
= Controller(loop
=self
.loop
)
1560 # Connect to controller
1561 await self
.controller
.connect(
1563 username
=self
.username
,
1564 password
=self
.secret
,
1565 cacert
=self
.ca_cert
,
1567 self
._authenticated
= True
1568 self
.log
.info("juju controller connected")
1569 except Exception as e
:
1570 message
= "Exception connecting to juju: {}".format(e
)
1571 self
.log
.error(message
)
1572 raise N2VCConnectionException(message
=message
, url
=self
.url
)
1574 self
._connecting
= False
1576 async def _juju_logout(self
):
1577 """Logout of the Juju controller."""
1578 if not self
._authenticated
:
1581 # disconnect all models
1582 for model_name
in self
.juju_models
:
1584 await self
._juju
_disconnect
_model
(model_name
)
1585 except Exception as e
:
1587 "Error disconnecting model {} : {}".format(model_name
, e
)
1589 # continue with next model...
1591 self
.log
.info("Disconnecting controller")
1593 await self
.controller
.disconnect()
1594 except Exception as e
:
1595 raise N2VCConnectionException(
1596 message
="Error disconnecting controller: {}".format(e
), url
=self
.url
1599 self
.controller
= None
1600 self
._authenticated
= False
1601 self
.log
.info("disconnected")
1603 async def _juju_disconnect_model(self
, model_name
: str):
1604 self
.log
.debug("Disconnecting model {}".format(model_name
))
1605 if model_name
in self
.juju_models
:
1606 await self
.juju_models
[model_name
].disconnect()
1607 self
.juju_models
[model_name
] = None
1608 self
.juju_observers
[model_name
] = None
1610 self
.warning("Cannot disconnect model: {}".format(model_name
))
1612 def _create_juju_public_key(self
):
1613 """Recreate the Juju public key on lcm container, if needed
1614 Certain libjuju commands expect to be run from the same machine as Juju
1615 is bootstrapped to. This method will write the public key to disk in
1616 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1619 # Make sure that we have a public key before writing to disk
1620 if self
.public_key
is None or len(self
.public_key
) == 0:
1621 if "OSMLCM_VCA_PUBKEY" in os
.environ
:
1622 self
.public_key
= os
.getenv("OSMLCM_VCA_PUBKEY", "")
1623 if len(self
.public_key
) == 0:
1628 pk_path
= "{}/.local/share/juju/ssh".format(os
.path
.expanduser("~"))
1629 file_path
= "{}/juju_id_rsa.pub".format(pk_path
)
1631 "writing juju public key to file:\n{}\npublic key: {}".format(
1632 file_path
, self
.public_key
1635 if not os
.path
.exists(pk_path
):
1636 # create path and write file
1637 os
.makedirs(pk_path
)
1638 with
open(file_path
, "w") as f
:
1639 self
.log
.debug("Creating juju public key file: {}".format(file_path
))
1640 f
.write(self
.public_key
)
1642 self
.log
.debug("juju public key file already exists: {}".format(file_path
))
1645 def _format_model_name(name
: str) -> str:
1646 """Format the name of the model.
1648 Model names may only contain lowercase letters, digits and hyphens
1651 return name
.replace("_", "-").replace(" ", "-").lower()
1654 def _format_app_name(name
: str) -> str:
1655 """Format the name of the application (in order to assure valid application name).
1657 Application names have restrictions (run juju deploy --help):
1658 - contains lowercase letters 'a'-'z'
1659 - contains numbers '0'-'9'
1660 - contains hyphens '-'
1661 - starts with a lowercase letter
1662 - not two or more consecutive hyphens
1663 - after a hyphen, not a group with all numbers
1666 def all_numbers(s
: str) -> bool:
1672 new_name
= name
.replace("_", "-")
1673 new_name
= new_name
.replace(" ", "-")
1674 new_name
= new_name
.lower()
1675 while new_name
.find("--") >= 0:
1676 new_name
= new_name
.replace("--", "-")
1677 groups
= new_name
.split("-")
1679 # find 'all numbers' groups and prefix them with a letter
1681 for i
in range(len(groups
)):
1683 if all_numbers(group
):
1689 if app_name
[0].isdigit():
1690 app_name
= "z" + app_name