blob: 690d3bee57cc98ae451e7f3c414849068022792e [file] [log] [blame]
quilesj29114342019-10-29 09:30:44 +01001##
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
quilesj29114342019-10-29 09:30:44 +010023import asyncio
quilesj29114342019-10-29 09:30:44 +010024import base64
25import binascii
beierlmf52cb7c2020-04-21 16:36:35 -040026import logging
27import os
quilesj29114342019-10-29 09:30:44 +010028import re
beierlmf52cb7c2020-04-21 16:36:35 -040029import time
quilesj29114342019-10-29 09:30:44 +010030
beierlmf52cb7c2020-04-21 16:36:35 -040031from juju.action import Action
32from juju.application import Application
33from juju.client import client
34from juju.controller import Controller
35from juju.errors import JujuAPIError
36from juju.machine import Machine
37from juju.model import Model
38from n2vc.exceptions import (
39 N2VCBadArgumentsException,
40 N2VCException,
41 N2VCConnectionException,
42 N2VCExecutionException,
43 N2VCInvalidCertificate,
44 N2VCNotFound,
45 MethodNotImplemented,
Dominik Fleischmannb9513342020-06-09 11:57:14 +020046 JujuK8sProxycharmNotSupported,
beierlmf52cb7c2020-04-21 16:36:35 -040047)
48from n2vc.juju_observer import JujuModelObserver
quilesj29114342019-10-29 09:30:44 +010049from n2vc.n2vc_conn import N2VCConnector
quilesj776ab392019-12-12 16:10:54 +000050from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
David Garciae370f3b2020-04-06 12:42:26 +020051from n2vc.provisioner import AsyncSSHProvisioner
David Garcia4fee80e2020-05-13 12:18:38 +020052from n2vc.libjuju import Libjuju
quilesj29114342019-10-29 09:30:44 +010053
54
55class N2VCJujuConnector(N2VCConnector):
56
57 """
David Garcia4fee80e2020-05-13 12:18:38 +020058####################################################################################
59################################### P U B L I C ####################################
60####################################################################################
quilesj29114342019-10-29 09:30:44 +010061 """
62
David Garcia347aae62020-04-29 12:34:23 +020063 BUILT_IN_CLOUDS = ["localhost", "microk8s"]
64
quilesj29114342019-10-29 09:30:44 +010065 def __init__(
66 self,
67 db: object,
68 fs: object,
69 log: object = None,
70 loop: object = None,
beierlmf52cb7c2020-04-21 16:36:35 -040071 url: str = "127.0.0.1:17070",
72 username: str = "admin",
quilesj29114342019-10-29 09:30:44 +010073 vca_config: dict = None,
beierlmf52cb7c2020-04-21 16:36:35 -040074 on_update_db=None,
quilesj29114342019-10-29 09:30:44 +010075 ):
76 """Initialize juju N2VC connector
77 """
78
79 # parent class constructor
80 N2VCConnector.__init__(
81 self,
82 db=db,
83 fs=fs,
84 log=log,
85 loop=loop,
86 url=url,
87 username=username,
88 vca_config=vca_config,
beierlmf52cb7c2020-04-21 16:36:35 -040089 on_update_db=on_update_db,
quilesj29114342019-10-29 09:30:44 +010090 )
91
92 # silence websocket traffic log
beierlmf52cb7c2020-04-21 16:36:35 -040093 logging.getLogger("websockets.protocol").setLevel(logging.INFO)
94 logging.getLogger("juju.client.connection").setLevel(logging.WARN)
95 logging.getLogger("model").setLevel(logging.WARN)
quilesj29114342019-10-29 09:30:44 +010096
beierlmf52cb7c2020-04-21 16:36:35 -040097 self.log.info("Initializing N2VC juju connector...")
quilesj29114342019-10-29 09:30:44 +010098
99 """
100 ##############################################################
101 # check arguments
102 ##############################################################
103 """
104
105 # juju URL
106 if url is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400107 raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
108 url_parts = url.split(":")
quilesj29114342019-10-29 09:30:44 +0100109 if len(url_parts) != 2:
beierlmf52cb7c2020-04-21 16:36:35 -0400110 raise N2VCBadArgumentsException(
111 "Argument url: bad format (localhost:port) -> {}".format(url), ["url"]
112 )
quilesj29114342019-10-29 09:30:44 +0100113 self.hostname = url_parts[0]
114 try:
115 self.port = int(url_parts[1])
116 except ValueError:
beierlmf52cb7c2020-04-21 16:36:35 -0400117 raise N2VCBadArgumentsException(
118 "url port must be a number -> {}".format(url), ["url"]
119 )
quilesj29114342019-10-29 09:30:44 +0100120
121 # juju USERNAME
122 if username is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400123 raise N2VCBadArgumentsException(
124 "Argument username is mandatory", ["username"]
125 )
quilesj29114342019-10-29 09:30:44 +0100126
127 # juju CONFIGURATION
128 if vca_config is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400129 raise N2VCBadArgumentsException(
130 "Argument vca_config is mandatory", ["vca_config"]
131 )
quilesj29114342019-10-29 09:30:44 +0100132
beierlmf52cb7c2020-04-21 16:36:35 -0400133 if "secret" in vca_config:
134 self.secret = vca_config["secret"]
quilesj29114342019-10-29 09:30:44 +0100135 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400136 raise N2VCBadArgumentsException(
137 "Argument vca_config.secret is mandatory", ["vca_config.secret"]
138 )
quilesj29114342019-10-29 09:30:44 +0100139
140 # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
141 # if exists, it will be written in lcm container: _create_juju_public_key()
beierlmf52cb7c2020-04-21 16:36:35 -0400142 if "public_key" in vca_config:
143 self.public_key = vca_config["public_key"]
quilesj29114342019-10-29 09:30:44 +0100144 else:
145 self.public_key = None
146
147 # TODO: Verify ca_cert is valid before using. VCA will crash
148 # if the ca_cert isn't formatted correctly.
149 def base64_to_cacert(b64string):
150 """Convert the base64-encoded string containing the VCA CACERT.
151
152 The input string....
153
154 """
155 try:
156 cacert = base64.b64decode(b64string).decode("utf-8")
157
beierlmf52cb7c2020-04-21 16:36:35 -0400158 cacert = re.sub(r"\\n", r"\n", cacert,)
quilesj29114342019-10-29 09:30:44 +0100159 except binascii.Error as e:
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100160 self.log.debug("Caught binascii.Error: {}".format(e))
quilesj29114342019-10-29 09:30:44 +0100161 raise N2VCInvalidCertificate(message="Invalid CA Certificate")
162
163 return cacert
164
beierlmf52cb7c2020-04-21 16:36:35 -0400165 self.ca_cert = vca_config.get("ca_cert")
quilesj29114342019-10-29 09:30:44 +0100166 if self.ca_cert:
beierlmf52cb7c2020-04-21 16:36:35 -0400167 self.ca_cert = base64_to_cacert(vca_config["ca_cert"])
quilesj29114342019-10-29 09:30:44 +0100168
David Garcia81045962020-07-16 12:37:13 +0200169 if "api_proxy" in vca_config and vca_config["api_proxy"] != "":
beierlmf52cb7c2020-04-21 16:36:35 -0400170 self.api_proxy = vca_config["api_proxy"]
171 self.log.debug(
172 "api_proxy for native charms configured: {}".format(self.api_proxy)
173 )
quilesj29114342019-10-29 09:30:44 +0100174 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400175 self.warning(
David Garcia81045962020-07-16 12:37:13 +0200176 "api_proxy is not configured"
beierlmf52cb7c2020-04-21 16:36:35 -0400177 )
tiernoec8a5042020-06-24 13:57:10 +0000178 self.api_proxy = None
quilesj29114342019-10-29 09:30:44 +0100179
beierlmf52cb7c2020-04-21 16:36:35 -0400180 if "enable_os_upgrade" in vca_config:
181 self.enable_os_upgrade = vca_config["enable_os_upgrade"]
garciadeblas923510c2019-12-17 15:02:11 +0100182 else:
183 self.enable_os_upgrade = True
184
beierlmf52cb7c2020-04-21 16:36:35 -0400185 if "apt_mirror" in vca_config:
186 self.apt_mirror = vca_config["apt_mirror"]
garciadeblas923510c2019-12-17 15:02:11 +0100187 else:
188 self.apt_mirror = None
189
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200190 self.cloud = vca_config.get('cloud')
191 self.k8s_cloud = None
192 if "k8s_cloud" in vca_config:
193 self.k8s_cloud = vca_config.get("k8s_cloud")
194 self.log.debug('Arguments have been checked')
quilesj29114342019-10-29 09:30:44 +0100195
196 # juju data
beierlmf52cb7c2020-04-21 16:36:35 -0400197 self.controller = None # it will be filled when connect to juju
198 self.juju_models = {} # model objects for every model_name
199 self.juju_observers = {} # model observers for every model_name
200 self._connecting = (
201 False # while connecting to juju (to avoid duplicate connections)
202 )
203 self._authenticated = (
204 False # it will be True when juju connection be stablished
205 )
206 self._creating_model = False # True during model creation
David Garcia4fee80e2020-05-13 12:18:38 +0200207 self.libjuju = Libjuju(
208 endpoint=self.url,
209 api_proxy=self.api_proxy,
210 enable_os_upgrade=self.enable_os_upgrade,
211 apt_mirror=self.apt_mirror,
212 username=self.username,
213 password=self.secret,
214 cacert=self.ca_cert,
215 loop=self.loop,
216 log=self.log,
217 db=self.db,
218 n2vc=self,
219 )
quilesj29114342019-10-29 09:30:44 +0100220
beierlmf52cb7c2020-04-21 16:36:35 -0400221 # create juju pub key file in lcm container at
222 # ./local/share/juju/ssh/juju_id_rsa.pub
quilesj29114342019-10-29 09:30:44 +0100223 self._create_juju_public_key()
224
beierlmf52cb7c2020-04-21 16:36:35 -0400225 self.log.info("N2VC juju connector initialized")
quilesj29114342019-10-29 09:30:44 +0100226
quilesj776ab392019-12-12 16:10:54 +0000227 async def get_status(self, namespace: str, yaml_format: bool = True):
228
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100229 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
quilesj29114342019-10-29 09:30:44 +0100230
beierlmf52cb7c2020-04-21 16:36:35 -0400231 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
232 namespace=namespace
233 )
quilesj29114342019-10-29 09:30:44 +0100234 # model name is ns_id
235 model_name = ns_id
236 if model_name is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400237 msg = "Namespace {} not valid".format(namespace)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100238 self.log.error(msg)
beierlmf52cb7c2020-04-21 16:36:35 -0400239 raise N2VCBadArgumentsException(msg, ["namespace"])
quilesj29114342019-10-29 09:30:44 +0100240
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200241 status = {}
242 models = await self.libjuju.list_models(contains=ns_id)
243
244 for m in models:
David Garciad745e222020-06-30 08:39:26 +0200245 status[m] = await self.libjuju.get_model_status(m)
quilesj29114342019-10-29 09:30:44 +0100246
quilesj776ab392019-12-12 16:10:54 +0000247 if yaml_format:
248 return obj_to_yaml(status)
249 else:
250 return obj_to_dict(status)
quilesj29114342019-10-29 09:30:44 +0100251
252 async def create_execution_environment(
253 self,
254 namespace: str,
255 db_dict: dict,
256 reuse_ee_id: str = None,
257 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400258 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100259 ) -> (str, dict):
260
beierlmf52cb7c2020-04-21 16:36:35 -0400261 self.log.info(
262 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
263 namespace, reuse_ee_id
264 )
265 )
quilesj29114342019-10-29 09:30:44 +0100266
quilesj29114342019-10-29 09:30:44 +0100267 machine_id = None
268 if reuse_ee_id:
beierlmf52cb7c2020-04-21 16:36:35 -0400269 model_name, application_name, machine_id = self._get_ee_id_components(
270 ee_id=reuse_ee_id
271 )
quilesj29114342019-10-29 09:30:44 +0100272 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400273 (
274 _nsi_id,
275 ns_id,
276 _vnf_id,
277 _vdu_id,
278 _vdu_count,
279 ) = self._get_namespace_components(namespace=namespace)
quilesj29114342019-10-29 09:30:44 +0100280 # model name is ns_id
281 model_name = ns_id
282 # application name
283 application_name = self._get_application_name(namespace=namespace)
284
beierlmf52cb7c2020-04-21 16:36:35 -0400285 self.log.debug(
286 "model name: {}, application name: {}, machine_id: {}".format(
287 model_name, application_name, machine_id
288 )
289 )
quilesj29114342019-10-29 09:30:44 +0100290
291 # create or reuse a new juju machine
292 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200293 if not await self.libjuju.model_exists(model_name):
294 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
295 machine, new = await self.libjuju.create_machine(
quilesj29114342019-10-29 09:30:44 +0100296 model_name=model_name,
quilesj29114342019-10-29 09:30:44 +0100297 machine_id=machine_id,
298 db_dict=db_dict,
299 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400300 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100301 )
David Garcia4fee80e2020-05-13 12:18:38 +0200302 # id for the execution environment
303 ee_id = N2VCJujuConnector._build_ee_id(
304 model_name=model_name,
305 application_name=application_name,
306 machine_id=str(machine.entity_id),
307 )
308 self.log.debug("ee_id: {}".format(ee_id))
309
310 if new:
311 # write ee_id in database
312 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
313
quilesj29114342019-10-29 09:30:44 +0100314 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400315 message = "Error creating machine on juju: {}".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100316 self.log.error(message)
quilesj29114342019-10-29 09:30:44 +0100317 raise N2VCException(message=message)
318
quilesj29114342019-10-29 09:30:44 +0100319 # new machine credentials
David Garcia4fee80e2020-05-13 12:18:38 +0200320 credentials = {
321 "hostname": machine.dns_name,
322 }
quilesj29114342019-10-29 09:30:44 +0100323
beierlmf52cb7c2020-04-21 16:36:35 -0400324 self.log.info(
325 "Execution environment created. ee_id: {}, credentials: {}".format(
326 ee_id, credentials
327 )
328 )
quilesj29114342019-10-29 09:30:44 +0100329
330 return ee_id, credentials
331
332 async def register_execution_environment(
333 self,
334 namespace: str,
335 credentials: dict,
336 db_dict: dict,
337 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400338 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100339 ) -> str:
340
beierlmf52cb7c2020-04-21 16:36:35 -0400341 self.log.info(
342 "Registering execution environment. namespace={}, credentials={}".format(
343 namespace, credentials
344 )
345 )
quilesj29114342019-10-29 09:30:44 +0100346
347 if credentials is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400348 raise N2VCBadArgumentsException(
349 message="credentials are mandatory", bad_args=["credentials"]
350 )
351 if credentials.get("hostname"):
352 hostname = credentials["hostname"]
quilesj29114342019-10-29 09:30:44 +0100353 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400354 raise N2VCBadArgumentsException(
355 message="hostname is mandatory", bad_args=["credentials.hostname"]
356 )
357 if credentials.get("username"):
358 username = credentials["username"]
quilesj29114342019-10-29 09:30:44 +0100359 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400360 raise N2VCBadArgumentsException(
361 message="username is mandatory", bad_args=["credentials.username"]
362 )
363 if "private_key_path" in credentials:
364 private_key_path = credentials["private_key_path"]
quilesj29114342019-10-29 09:30:44 +0100365 else:
366 # if not passed as argument, use generated private key path
367 private_key_path = self.private_key_path
368
beierlmf52cb7c2020-04-21 16:36:35 -0400369 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
370 namespace=namespace
371 )
quilesj29114342019-10-29 09:30:44 +0100372
373 # model name
374 model_name = ns_id
375 # application name
376 application_name = self._get_application_name(namespace=namespace)
377
378 # register machine on juju
379 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200380 if not await self.libjuju.model_exists(model_name):
381 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
382 machine_id = await self.libjuju.provision_machine(
quilesj29114342019-10-29 09:30:44 +0100383 model_name=model_name,
384 hostname=hostname,
385 username=username,
386 private_key_path=private_key_path,
387 db_dict=db_dict,
388 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400389 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100390 )
391 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400392 self.log.error("Error registering machine: {}".format(e))
393 raise N2VCException(
394 message="Error registering machine on juju: {}".format(e)
395 )
quilesjac4e0de2019-11-27 16:12:02 +0000396
beierlmf52cb7c2020-04-21 16:36:35 -0400397 self.log.info("Machine registered: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +0100398
399 # id for the execution environment
400 ee_id = N2VCJujuConnector._build_ee_id(
401 model_name=model_name,
402 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400403 machine_id=str(machine_id),
quilesj29114342019-10-29 09:30:44 +0100404 )
405
beierlmf52cb7c2020-04-21 16:36:35 -0400406 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100407
408 return ee_id
409
410 async def install_configuration_sw(
411 self,
412 ee_id: str,
413 artifact_path: str,
414 db_dict: dict,
415 progress_timeout: float = None,
David Garciadfaa6e82020-04-01 16:06:39 +0200416 total_timeout: float = None,
417 config: dict = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100418 num_units: int = 1,
quilesj29114342019-10-29 09:30:44 +0100419 ):
420
beierlmf52cb7c2020-04-21 16:36:35 -0400421 self.log.info(
422 (
423 "Installing configuration sw on ee_id: {}, "
424 "artifact path: {}, db_dict: {}"
425 ).format(ee_id, artifact_path, db_dict)
426 )
quilesj29114342019-10-29 09:30:44 +0100427
quilesj29114342019-10-29 09:30:44 +0100428 # check arguments
429 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400430 raise N2VCBadArgumentsException(
431 message="ee_id is mandatory", bad_args=["ee_id"]
432 )
quilesj29114342019-10-29 09:30:44 +0100433 if artifact_path is None or len(artifact_path) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400434 raise N2VCBadArgumentsException(
435 message="artifact_path is mandatory", bad_args=["artifact_path"]
436 )
quilesj29114342019-10-29 09:30:44 +0100437 if db_dict is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400438 raise N2VCBadArgumentsException(
439 message="db_dict is mandatory", bad_args=["db_dict"]
440 )
quilesj29114342019-10-29 09:30:44 +0100441
442 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400443 (
444 model_name,
445 application_name,
446 machine_id,
447 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
448 self.log.debug(
449 "model: {}, application: {}, machine: {}".format(
450 model_name, application_name, machine_id
451 )
452 )
453 except Exception:
quilesj29114342019-10-29 09:30:44 +0100454 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400455 message="ee_id={} is not a valid execution environment id".format(
456 ee_id
457 ),
458 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100459 )
460
461 # remove // in charm path
beierlmf52cb7c2020-04-21 16:36:35 -0400462 while artifact_path.find("//") >= 0:
463 artifact_path = artifact_path.replace("//", "/")
quilesj29114342019-10-29 09:30:44 +0100464
465 # check charm path
466 if not self.fs.file_exists(artifact_path, mode="dir"):
beierlmf52cb7c2020-04-21 16:36:35 -0400467 msg = "artifact path does not exist: {}".format(artifact_path)
468 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
quilesj29114342019-10-29 09:30:44 +0100469
beierlmf52cb7c2020-04-21 16:36:35 -0400470 if artifact_path.startswith("/"):
quilesj29114342019-10-29 09:30:44 +0100471 full_path = self.fs.path + artifact_path
472 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400473 full_path = self.fs.path + "/" + artifact_path
quilesj29114342019-10-29 09:30:44 +0100474
475 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200476 await self.libjuju.deploy_charm(
quilesj29114342019-10-29 09:30:44 +0100477 model_name=model_name,
478 application_name=application_name,
David Garcia4fee80e2020-05-13 12:18:38 +0200479 path=full_path,
quilesj29114342019-10-29 09:30:44 +0100480 machine_id=machine_id,
481 db_dict=db_dict,
482 progress_timeout=progress_timeout,
David Garciadfaa6e82020-04-01 16:06:39 +0200483 total_timeout=total_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400484 config=config,
David Garciaf8a9d462020-03-25 18:19:02 +0100485 num_units=num_units,
quilesj29114342019-10-29 09:30:44 +0100486 )
487 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400488 raise N2VCException(
489 message="Error desploying charm into ee={} : {}".format(ee_id, e)
490 )
quilesj29114342019-10-29 09:30:44 +0100491
beierlmf52cb7c2020-04-21 16:36:35 -0400492 self.log.info("Configuration sw installed")
quilesj29114342019-10-29 09:30:44 +0100493
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200494 async def install_k8s_proxy_charm(
495 self,
496 charm_name: str,
497 namespace: str,
498 artifact_path: str,
499 db_dict: dict,
500 progress_timeout: float = None,
501 total_timeout: float = None,
502 config: dict = None,
503 ) -> str:
504 """
505 Install a k8s proxy charm
506
507 :param charm_name: Name of the charm being deployed
508 :param namespace: collection of all the uuids related to the charm.
509 :param str artifact_path: where to locate the artifacts (parent folder) using
510 the self.fs
511 the final artifact path will be a combination of this artifact_path and
512 additional string from the config_dict (e.g. charm name)
513 :param dict db_dict: where to write into database when the status changes.
514 It contains a dict with
515 {collection: <str>, filter: {}, path: <str>},
516 e.g. {collection: "nsrs", filter:
517 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
518 :param float progress_timeout:
519 :param float total_timeout:
520 :param config: Dictionary with additional configuration
521
522 :returns ee_id: execution environment id.
523 """
524 self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
525 .format(charm_name, artifact_path, db_dict))
526
527 if not self.k8s_cloud:
528 raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
529
530 if artifact_path is None or len(artifact_path) == 0:
531 raise N2VCBadArgumentsException(
532 message="artifact_path is mandatory", bad_args=["artifact_path"]
533 )
534 if db_dict is None:
535 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
536
537 # remove // in charm path
538 while artifact_path.find('//') >= 0:
539 artifact_path = artifact_path.replace('//', '/')
540
541 # check charm path
542 if not self.fs.file_exists(artifact_path, mode="dir"):
543 msg = 'artifact path does not exist: {}'.format(artifact_path)
544 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
545
546 if artifact_path.startswith('/'):
547 full_path = self.fs.path + artifact_path
548 else:
549 full_path = self.fs.path + '/' + artifact_path
550
551 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
552 model_name = '{}-k8s'.format(ns_id)
553
554 await self.libjuju.add_model(model_name, self.k8s_cloud)
555 application_name = self._get_application_name(namespace)
556
557 try:
558 await self.libjuju.deploy_charm(
559 model_name=model_name,
560 application_name=application_name,
561 path=full_path,
562 machine_id=None,
563 db_dict=db_dict,
564 progress_timeout=progress_timeout,
565 total_timeout=total_timeout,
566 config=config
567 )
568 except Exception as e:
569 raise N2VCException(message='Error deploying charm: {}'.format(e))
570
571 self.log.info('K8s proxy charm installed')
572 ee_id = N2VCJujuConnector._build_ee_id(
573 model_name=model_name,
574 application_name=application_name,
575 machine_id="k8s",
576 )
Dominik Fleischmann7ace6fa2020-06-29 16:16:28 +0200577
578 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
579
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200580 return ee_id
581
quilesj29114342019-10-29 09:30:44 +0100582 async def get_ee_ssh_public__key(
583 self,
584 ee_id: str,
585 db_dict: dict,
586 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400587 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100588 ) -> str:
589
beierlmf52cb7c2020-04-21 16:36:35 -0400590 self.log.info(
591 (
592 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
593 ).format(ee_id, db_dict)
594 )
quilesj29114342019-10-29 09:30:44 +0100595
quilesj29114342019-10-29 09:30:44 +0100596 # check arguments
597 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400598 raise N2VCBadArgumentsException(
599 message="ee_id is mandatory", bad_args=["ee_id"]
600 )
quilesj29114342019-10-29 09:30:44 +0100601 if db_dict is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400602 raise N2VCBadArgumentsException(
603 message="db_dict is mandatory", bad_args=["db_dict"]
604 )
quilesj29114342019-10-29 09:30:44 +0100605
606 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400607 (
608 model_name,
609 application_name,
610 machine_id,
611 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
612 self.log.debug(
613 "model: {}, application: {}, machine: {}".format(
614 model_name, application_name, machine_id
615 )
616 )
617 except Exception:
quilesj29114342019-10-29 09:30:44 +0100618 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400619 message="ee_id={} is not a valid execution environment id".format(
620 ee_id
621 ),
622 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100623 )
624
625 # try to execute ssh layer primitives (if exist):
626 # generate-ssh-key
627 # get-ssh-public-key
628
629 output = None
630
David Garcia4fee80e2020-05-13 12:18:38 +0200631 application_name = N2VCJujuConnector._format_app_name(application_name)
632
quilesj29114342019-10-29 09:30:44 +0100633 # execute action: generate-ssh-key
634 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200635 output, _status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100636 model_name=model_name,
637 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400638 action_name="generate-ssh-key",
quilesj29114342019-10-29 09:30:44 +0100639 db_dict=db_dict,
640 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400641 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100642 )
643 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400644 self.log.info(
645 "Skipping exception while executing action generate-ssh-key: {}".format(
646 e
647 )
648 )
quilesj29114342019-10-29 09:30:44 +0100649
650 # execute action: get-ssh-public-key
651 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200652 output, _status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100653 model_name=model_name,
654 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400655 action_name="get-ssh-public-key",
quilesj29114342019-10-29 09:30:44 +0100656 db_dict=db_dict,
657 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400658 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100659 )
660 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400661 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100662 self.log.info(msg)
David Garcia4fee80e2020-05-13 12:18:38 +0200663 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
quilesj29114342019-10-29 09:30:44 +0100664
665 # return public key if exists
David Garcia9ae8fa52019-12-09 18:50:03 +0100666 return output["pubkey"] if "pubkey" in output else output
quilesj29114342019-10-29 09:30:44 +0100667
668 async def add_relation(
beierlmf52cb7c2020-04-21 16:36:35 -0400669 self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
quilesj29114342019-10-29 09:30:44 +0100670 ):
671
beierlmf52cb7c2020-04-21 16:36:35 -0400672 self.log.debug(
673 "adding new relation between {} and {}, endpoints: {}, {}".format(
674 ee_id_1, ee_id_2, endpoint_1, endpoint_2
675 )
676 )
quilesj29114342019-10-29 09:30:44 +0100677
quilesjaae10b42020-01-09 08:49:10 +0000678 # check arguments
679 if not ee_id_1:
beierlmf52cb7c2020-04-21 16:36:35 -0400680 message = "EE 1 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100681 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400682 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
quilesjaae10b42020-01-09 08:49:10 +0000683 if not ee_id_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400684 message = "EE 2 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100685 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400686 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
quilesjaae10b42020-01-09 08:49:10 +0000687 if not endpoint_1:
beierlmf52cb7c2020-04-21 16:36:35 -0400688 message = "endpoint 1 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100689 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400690 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
quilesjaae10b42020-01-09 08:49:10 +0000691 if not endpoint_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400692 message = "endpoint 2 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100693 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400694 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
quilesjaae10b42020-01-09 08:49:10 +0000695
quilesjaae10b42020-01-09 08:49:10 +0000696 # get the model, the applications and the machines from the ee_id's
beierlmf52cb7c2020-04-21 16:36:35 -0400697 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
698 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
quilesj29114342019-10-29 09:30:44 +0100699
700 # model must be the same
701 if model_1 != model_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400702 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100703 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400704 raise N2VCBadArgumentsException(
705 message=message, bad_args=["ee_id_1", "ee_id_2"]
706 )
quilesj29114342019-10-29 09:30:44 +0100707
708 # add juju relations between two applications
709 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200710 await self.libjuju.add_relation(
quilesjaae10b42020-01-09 08:49:10 +0000711 model_name=model_1,
712 application_name_1=app_1,
713 application_name_2=app_2,
714 relation_1=endpoint_1,
beierlmf52cb7c2020-04-21 16:36:35 -0400715 relation_2=endpoint_2,
quilesjaae10b42020-01-09 08:49:10 +0000716 )
quilesj29114342019-10-29 09:30:44 +0100717 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400718 message = "Error adding relation between {} and {}: {}".format(
719 ee_id_1, ee_id_2, e
720 )
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100721 self.log.error(message)
quilesj29114342019-10-29 09:30:44 +0100722 raise N2VCException(message=message)
723
beierlmf52cb7c2020-04-21 16:36:35 -0400724 async def remove_relation(self):
quilesj29114342019-10-29 09:30:44 +0100725 # TODO
beierlmf52cb7c2020-04-21 16:36:35 -0400726 self.log.info("Method not implemented yet")
727 raise MethodNotImplemented()
quilesj29114342019-10-29 09:30:44 +0100728
beierlmf52cb7c2020-04-21 16:36:35 -0400729 async def deregister_execution_environments(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400730 self.log.info("Method not implemented yet")
731 raise MethodNotImplemented()
quilesj29114342019-10-29 09:30:44 +0100732
733 async def delete_namespace(
beierlmf52cb7c2020-04-21 16:36:35 -0400734 self, namespace: str, db_dict: dict = None, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +0100735 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400736 self.log.info("Deleting namespace={}".format(namespace))
quilesj29114342019-10-29 09:30:44 +0100737
quilesj29114342019-10-29 09:30:44 +0100738 # check arguments
739 if namespace is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400740 raise N2VCBadArgumentsException(
741 message="namespace is mandatory", bad_args=["namespace"]
742 )
quilesj29114342019-10-29 09:30:44 +0100743
beierlmf52cb7c2020-04-21 16:36:35 -0400744 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
745 namespace=namespace
746 )
quilesj29114342019-10-29 09:30:44 +0100747 if ns_id is not None:
quilesj29114342019-10-29 09:30:44 +0100748 try:
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200749 models = await self.libjuju.list_models(contains=ns_id)
750 for model in models:
751 await self.libjuju.destroy_model(
752 model_name=model, total_timeout=total_timeout
753 )
quilesj29114342019-10-29 09:30:44 +0100754 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400755 raise N2VCException(
756 message="Error deleting namespace {} : {}".format(namespace, e)
757 )
quilesj29114342019-10-29 09:30:44 +0100758 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400759 raise N2VCBadArgumentsException(
760 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
761 )
quilesj29114342019-10-29 09:30:44 +0100762
beierlmf52cb7c2020-04-21 16:36:35 -0400763 self.log.info("Namespace {} deleted".format(namespace))
quilesj29114342019-10-29 09:30:44 +0100764
765 async def delete_execution_environment(
beierlmf52cb7c2020-04-21 16:36:35 -0400766 self, ee_id: str, db_dict: dict = None, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +0100767 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400768 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100769
quilesj29114342019-10-29 09:30:44 +0100770 # check arguments
771 if ee_id is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400772 raise N2VCBadArgumentsException(
773 message="ee_id is mandatory", bad_args=["ee_id"]
774 )
quilesj29114342019-10-29 09:30:44 +0100775
beierlmf52cb7c2020-04-21 16:36:35 -0400776 model_name, application_name, _machine_id = self._get_ee_id_components(
777 ee_id=ee_id
778 )
quilesj29114342019-10-29 09:30:44 +0100779
780 # destroy the application
781 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200782 await self.libjuju.destroy_model(
783 model_name=model_name, total_timeout=total_timeout
beierlmf52cb7c2020-04-21 16:36:35 -0400784 )
quilesj29114342019-10-29 09:30:44 +0100785 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400786 raise N2VCException(
787 message=(
788 "Error deleting execution environment {} (application {}) : {}"
789 ).format(ee_id, application_name, e)
790 )
quilesj29114342019-10-29 09:30:44 +0100791
792 # destroy the machine
beierlmf52cb7c2020-04-21 16:36:35 -0400793 # try:
calvinosanch41395222020-02-21 09:25:21 +0100794 # await self._juju_destroy_machine(
795 # model_name=model_name,
796 # machine_id=machine_id,
797 # total_timeout=total_timeout
798 # )
799 # except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400800 # raise N2VCException(
801 # message='Error deleting execution environment {} (machine {}) : {}'
802 # .format(ee_id, machine_id, e))
quilesj29114342019-10-29 09:30:44 +0100803
beierlmf52cb7c2020-04-21 16:36:35 -0400804 self.log.info("Execution environment {} deleted".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100805
806 async def exec_primitive(
beierlmf52cb7c2020-04-21 16:36:35 -0400807 self,
808 ee_id: str,
809 primitive_name: str,
810 params_dict: dict,
811 db_dict: dict = None,
812 progress_timeout: float = None,
813 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100814 ) -> str:
815
beierlmf52cb7c2020-04-21 16:36:35 -0400816 self.log.info(
817 "Executing primitive: {} on ee: {}, params: {}".format(
818 primitive_name, ee_id, params_dict
819 )
820 )
quilesj29114342019-10-29 09:30:44 +0100821
quilesj29114342019-10-29 09:30:44 +0100822 # check arguments
823 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400824 raise N2VCBadArgumentsException(
825 message="ee_id is mandatory", bad_args=["ee_id"]
826 )
quilesj29114342019-10-29 09:30:44 +0100827 if primitive_name is None or len(primitive_name) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400828 raise N2VCBadArgumentsException(
829 message="action_name is mandatory", bad_args=["action_name"]
830 )
quilesj29114342019-10-29 09:30:44 +0100831 if params_dict is None:
832 params_dict = dict()
833
834 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400835 (
836 model_name,
837 application_name,
838 _machine_id,
839 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
quilesj29114342019-10-29 09:30:44 +0100840 except Exception:
841 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400842 message="ee_id={} is not a valid execution environment id".format(
843 ee_id
844 ),
845 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100846 )
847
beierlmf52cb7c2020-04-21 16:36:35 -0400848 if primitive_name == "config":
quilesj29114342019-10-29 09:30:44 +0100849 # Special case: config primitive
850 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200851 await self.libjuju.configure_application(
quilesj29114342019-10-29 09:30:44 +0100852 model_name=model_name,
853 application_name=application_name,
854 config=params_dict,
quilesj29114342019-10-29 09:30:44 +0100855 )
David Garcia4fee80e2020-05-13 12:18:38 +0200856 actions = await self.libjuju.get_actions(
857 application_name=application_name, model_name=model_name,
858 )
859 self.log.debug(
860 "Application {} has these actions: {}".format(
861 application_name, actions
862 )
863 )
864 if "verify-ssh-credentials" in actions:
865 # execute verify-credentials
866 num_retries = 20
867 retry_timeout = 15.0
868 for _ in range(num_retries):
869 try:
870 self.log.debug("Executing action verify-ssh-credentials...")
871 output, ok = await self.libjuju.execute_action(
872 model_name=model_name,
873 application_name=application_name,
874 action_name="verify-ssh-credentials",
875 db_dict=db_dict,
876 progress_timeout=progress_timeout,
877 total_timeout=total_timeout,
878 )
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200879
880 if ok == "failed":
881 self.log.debug(
882 "Error executing verify-ssh-credentials: {}. Retrying..."
883 )
884 await asyncio.sleep(retry_timeout)
885
886 continue
David Garcia4fee80e2020-05-13 12:18:38 +0200887 self.log.debug("Result: {}, output: {}".format(ok, output))
888 break
889 except asyncio.CancelledError:
890 raise
David Garcia4fee80e2020-05-13 12:18:38 +0200891 else:
892 self.log.error(
893 "Error executing verify-ssh-credentials after {} retries. ".format(
894 num_retries
895 )
896 )
897 else:
898 msg = "Action verify-ssh-credentials does not exist in application {}".format(
899 application_name
900 )
901 self.log.debug(msg=msg)
quilesj29114342019-10-29 09:30:44 +0100902 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400903 self.log.error("Error configuring juju application: {}".format(e))
quilesj29114342019-10-29 09:30:44 +0100904 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -0400905 message="Error configuring application into ee={} : {}".format(
906 ee_id, e
907 ),
908 primitive_name=primitive_name,
quilesj29114342019-10-29 09:30:44 +0100909 )
beierlmf52cb7c2020-04-21 16:36:35 -0400910 return "CONFIG OK"
quilesj29114342019-10-29 09:30:44 +0100911 else:
912 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200913 output, status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100914 model_name=model_name,
915 application_name=application_name,
916 action_name=primitive_name,
917 db_dict=db_dict,
918 progress_timeout=progress_timeout,
919 total_timeout=total_timeout,
920 **params_dict
921 )
beierlmf52cb7c2020-04-21 16:36:35 -0400922 if status == "completed":
quilesj29114342019-10-29 09:30:44 +0100923 return output
924 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400925 raise Exception("status is not completed: {}".format(status))
quilesj29114342019-10-29 09:30:44 +0100926 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400927 self.log.error(
928 "Error executing primitive {}: {}".format(primitive_name, e)
929 )
quilesj29114342019-10-29 09:30:44 +0100930 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -0400931 message="Error executing primitive {} into ee={} : {}".format(
932 primitive_name, ee_id, e
933 ),
934 primitive_name=primitive_name,
quilesj29114342019-10-29 09:30:44 +0100935 )
936
937 async def disconnect(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400938 self.log.info("closing juju N2VC...")
David Garcia4fee80e2020-05-13 12:18:38 +0200939 try:
940 await self.libjuju.disconnect()
941 except Exception as e:
942 raise N2VCConnectionException(
943 message="Error disconnecting controller: {}".format(e), url=self.url
944 )
quilesj29114342019-10-29 09:30:44 +0100945
946 """
David Garcia4fee80e2020-05-13 12:18:38 +0200947####################################################################################
948################################### P R I V A T E ##################################
949####################################################################################
quilesj29114342019-10-29 09:30:44 +0100950 """
951
beierlmf52cb7c2020-04-21 16:36:35 -0400952 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
quilesj29114342019-10-29 09:30:44 +0100953
954 # write ee_id to database: _admin.deployed.VCA.x
955 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400956 the_table = db_dict["collection"]
957 the_filter = db_dict["filter"]
958 the_path = db_dict["path"]
959 if not the_path[-1] == ".":
960 the_path = the_path + "."
961 update_dict = {the_path + "ee_id": ee_id}
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100962 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
quilesj29114342019-10-29 09:30:44 +0100963 self.db.set_one(
964 table=the_table,
965 q_filter=the_filter,
966 update_dict=update_dict,
beierlmf52cb7c2020-04-21 16:36:35 -0400967 fail_on_empty=True,
quilesj29114342019-10-29 09:30:44 +0100968 )
tierno8ff11992020-03-26 09:51:11 +0000969 except asyncio.CancelledError:
970 raise
quilesj29114342019-10-29 09:30:44 +0100971 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400972 self.log.error("Error writing ee_id to database: {}".format(e))
quilesj29114342019-10-29 09:30:44 +0100973
974 @staticmethod
beierlmf52cb7c2020-04-21 16:36:35 -0400975 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
quilesj29114342019-10-29 09:30:44 +0100976 """
977 Build an execution environment id form model, application and machine
978 :param model_name:
979 :param application_name:
980 :param machine_id:
981 :return:
982 """
983 # id for the execution environment
beierlmf52cb7c2020-04-21 16:36:35 -0400984 return "{}.{}.{}".format(model_name, application_name, machine_id)
quilesj29114342019-10-29 09:30:44 +0100985
986 @staticmethod
beierlmf52cb7c2020-04-21 16:36:35 -0400987 def _get_ee_id_components(ee_id: str) -> (str, str, str):
quilesj29114342019-10-29 09:30:44 +0100988 """
989 Get model, application and machine components from an execution environment id
990 :param ee_id:
991 :return: model_name, application_name, machine_id
992 """
993
994 if ee_id is None:
995 return None, None, None
996
997 # split components of id
beierlmf52cb7c2020-04-21 16:36:35 -0400998 parts = ee_id.split(".")
quilesj29114342019-10-29 09:30:44 +0100999 model_name = parts[0]
1000 application_name = parts[1]
1001 machine_id = parts[2]
1002 return model_name, application_name, machine_id
1003
1004 def _get_application_name(self, namespace: str) -> str:
1005 """
1006 Build application name from namespace
1007 :param namespace:
1008 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1009 """
1010
Adam Israel18046072019-12-08 21:44:29 -05001011 # TODO: Enforce the Juju 50-character application limit
1012
quilesj29114342019-10-29 09:30:44 +01001013 # split namespace components
beierlmf52cb7c2020-04-21 16:36:35 -04001014 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1015 namespace=namespace
1016 )
quilesj29114342019-10-29 09:30:44 +01001017
1018 if vnf_id is None or len(vnf_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001019 vnf_id = ""
quilesj29114342019-10-29 09:30:44 +01001020 else:
Adam Israel18046072019-12-08 21:44:29 -05001021 # Shorten the vnf_id to its last twelve characters
beierlmf52cb7c2020-04-21 16:36:35 -04001022 vnf_id = "vnf-" + vnf_id[-12:]
quilesj29114342019-10-29 09:30:44 +01001023
1024 if vdu_id is None or len(vdu_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001025 vdu_id = ""
quilesj29114342019-10-29 09:30:44 +01001026 else:
Adam Israel18046072019-12-08 21:44:29 -05001027 # Shorten the vdu_id to its last twelve characters
beierlmf52cb7c2020-04-21 16:36:35 -04001028 vdu_id = "-vdu-" + vdu_id[-12:]
quilesj29114342019-10-29 09:30:44 +01001029
1030 if vdu_count is None or len(vdu_count) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001031 vdu_count = ""
quilesj29114342019-10-29 09:30:44 +01001032 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001033 vdu_count = "-cnt-" + vdu_count
quilesj29114342019-10-29 09:30:44 +01001034
beierlmf52cb7c2020-04-21 16:36:35 -04001035 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
quilesj29114342019-10-29 09:30:44 +01001036
1037 return N2VCJujuConnector._format_app_name(application_name)
1038
1039 async def _juju_create_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001040 self,
1041 model_name: str,
1042 application_name: str,
1043 machine_id: str = None,
1044 db_dict: dict = None,
1045 progress_timeout: float = None,
1046 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +01001047 ) -> Machine:
1048
beierlmf52cb7c2020-04-21 16:36:35 -04001049 self.log.debug(
1050 "creating machine in model: {}, existing machine id: {}".format(
1051 model_name, machine_id
1052 )
1053 )
quilesj29114342019-10-29 09:30:44 +01001054
1055 # get juju model and observer (create model if needed)
1056 model = await self._juju_get_model(model_name=model_name)
1057 observer = self.juju_observers[model_name]
1058
1059 # find machine id in model
1060 machine = None
1061 if machine_id is not None:
beierlmf52cb7c2020-04-21 16:36:35 -04001062 self.log.debug("Finding existing machine id {} in model".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001063 # get juju existing machines in the model
1064 existing_machines = await model.get_machines()
1065 if machine_id in existing_machines:
beierlmf52cb7c2020-04-21 16:36:35 -04001066 self.log.debug(
1067 "Machine id {} found in model (reusing it)".format(machine_id)
1068 )
quilesj29114342019-10-29 09:30:44 +01001069 machine = model.machines[machine_id]
1070
1071 if machine is None:
beierlmf52cb7c2020-04-21 16:36:35 -04001072 self.log.debug("Creating a new machine in juju...")
quilesj29114342019-10-29 09:30:44 +01001073 # machine does not exist, create it and wait for it
1074 machine = await model.add_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001075 spec=None, constraints=None, disks=None, series="xenial"
quilesj29114342019-10-29 09:30:44 +01001076 )
1077
1078 # register machine with observer
1079 observer.register_machine(machine=machine, db_dict=db_dict)
1080
1081 # id for the execution environment
1082 ee_id = N2VCJujuConnector._build_ee_id(
1083 model_name=model_name,
1084 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001085 machine_id=str(machine.entity_id),
quilesj29114342019-10-29 09:30:44 +01001086 )
1087
1088 # write ee_id in database
beierlmf52cb7c2020-04-21 16:36:35 -04001089 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
quilesj29114342019-10-29 09:30:44 +01001090
1091 # wait for machine creation
1092 await observer.wait_for_machine(
1093 machine_id=str(machine.entity_id),
1094 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001095 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001096 )
1097
1098 else:
1099
beierlmf52cb7c2020-04-21 16:36:35 -04001100 self.log.debug("Reusing old machine pending")
quilesj29114342019-10-29 09:30:44 +01001101
1102 # register machine with observer
1103 observer.register_machine(machine=machine, db_dict=db_dict)
1104
beierlmf52cb7c2020-04-21 16:36:35 -04001105 # machine does exist, but it is in creation process (pending), wait for
1106 # create finalisation
quilesj29114342019-10-29 09:30:44 +01001107 await observer.wait_for_machine(
1108 machine_id=machine.entity_id,
1109 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001110 total_timeout=total_timeout,
1111 )
quilesj29114342019-10-29 09:30:44 +01001112
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001113 self.log.debug("Machine ready at " + str(machine.dns_name))
quilesj29114342019-10-29 09:30:44 +01001114 return machine
1115
1116 async def _juju_provision_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001117 self,
1118 model_name: str,
1119 hostname: str,
1120 username: str,
1121 private_key_path: str,
1122 db_dict: dict = None,
1123 progress_timeout: float = None,
1124 total_timeout: float = None,
quilesjac4e0de2019-11-27 16:12:02 +00001125 ) -> str:
quilesj29114342019-10-29 09:30:44 +01001126
quilesjac4e0de2019-11-27 16:12:02 +00001127 if not self.api_proxy:
beierlmf52cb7c2020-04-21 16:36:35 -04001128 msg = "Cannot provision machine: api_proxy is not defined"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001129 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001130 raise N2VCException(message=msg)
1131
beierlmf52cb7c2020-04-21 16:36:35 -04001132 self.log.debug(
1133 "provisioning machine. model: {}, hostname: {}, username: {}".format(
1134 model_name, hostname, username
1135 )
1136 )
quilesj29114342019-10-29 09:30:44 +01001137
1138 if not self._authenticated:
1139 await self._juju_login()
1140
1141 # get juju model and observer
1142 model = await self._juju_get_model(model_name=model_name)
1143 observer = self.juju_observers[model_name]
1144
quilesjac4e0de2019-11-27 16:12:02 +00001145 # TODO check if machine is already provisioned
1146 machine_list = await model.get_machines()
1147
David Garciae370f3b2020-04-06 12:42:26 +02001148 provisioner = AsyncSSHProvisioner(
quilesjac4e0de2019-11-27 16:12:02 +00001149 host=hostname,
1150 user=username,
1151 private_key_path=private_key_path,
beierlmf52cb7c2020-04-21 16:36:35 -04001152 log=self.log,
quilesjac4e0de2019-11-27 16:12:02 +00001153 )
1154
1155 params = None
quilesj29114342019-10-29 09:30:44 +01001156 try:
David Garciae370f3b2020-04-06 12:42:26 +02001157 params = await provisioner.provision_machine()
quilesjac4e0de2019-11-27 16:12:02 +00001158 except Exception as ex:
1159 msg = "Exception provisioning machine: {}".format(ex)
1160 self.log.error(msg)
1161 raise N2VCException(message=msg)
1162
beierlmf52cb7c2020-04-21 16:36:35 -04001163 params.jobs = ["JobHostUnits"]
quilesjac4e0de2019-11-27 16:12:02 +00001164
1165 connection = model.connection()
1166
1167 # Submit the request.
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001168 self.log.debug("Adding machine to model")
quilesjac4e0de2019-11-27 16:12:02 +00001169 client_facade = client.ClientFacade.from_connection(connection)
1170 results = await client_facade.AddMachines(params=[params])
1171 error = results.machines[0].error
1172 if error:
beierlm0a8c9af2020-05-12 15:26:37 -04001173 msg = "Error adding machine: {}".format(error.message)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001174 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001175 raise ValueError(msg)
1176
1177 machine_id = results.machines[0].machine
1178
1179 # Need to run this after AddMachines has been called,
1180 # as we need the machine_id
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001181 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
beierlmf52cb7c2020-04-21 16:36:35 -04001182 asyncio.ensure_future(
1183 provisioner.install_agent(
1184 connection=connection,
1185 nonce=params.nonce,
1186 machine_id=machine_id,
David Garcia81045962020-07-16 12:37:13 +02001187 proxy=self.api_proxy,
beierlmf52cb7c2020-04-21 16:36:35 -04001188 )
1189 )
quilesjac4e0de2019-11-27 16:12:02 +00001190
beierlmf52cb7c2020-04-21 16:36:35 -04001191 # wait for machine in model (now, machine is not yet in model, so we must
1192 # wait for it)
quilesjac4e0de2019-11-27 16:12:02 +00001193 machine = None
beierlmf52cb7c2020-04-21 16:36:35 -04001194 for _ in range(10):
quilesjac4e0de2019-11-27 16:12:02 +00001195 machine_list = await model.get_machines()
1196 if machine_id in machine_list:
beierlmf52cb7c2020-04-21 16:36:35 -04001197 self.log.debug("Machine {} found in model!".format(machine_id))
quilesjac4e0de2019-11-27 16:12:02 +00001198 machine = model.machines.get(machine_id)
1199 break
1200 await asyncio.sleep(2)
1201
1202 if machine is None:
beierlmf52cb7c2020-04-21 16:36:35 -04001203 msg = "Machine {} not found in model".format(machine_id)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001204 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001205 raise Exception(msg)
quilesj29114342019-10-29 09:30:44 +01001206
1207 # register machine with observer
1208 observer.register_machine(machine=machine, db_dict=db_dict)
1209
1210 # wait for machine creation
beierlmf52cb7c2020-04-21 16:36:35 -04001211 self.log.debug("waiting for provision finishes... {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001212 await observer.wait_for_machine(
quilesjac4e0de2019-11-27 16:12:02 +00001213 machine_id=machine_id,
quilesj29114342019-10-29 09:30:44 +01001214 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001215 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001216 )
1217
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001218 self.log.debug("Machine provisioned {}".format(machine_id))
quilesjac4e0de2019-11-27 16:12:02 +00001219
1220 return machine_id
quilesj29114342019-10-29 09:30:44 +01001221
1222 async def _juju_deploy_charm(
beierlmf52cb7c2020-04-21 16:36:35 -04001223 self,
1224 model_name: str,
1225 application_name: str,
1226 charm_path: str,
1227 machine_id: str,
1228 db_dict: dict,
1229 progress_timeout: float = None,
1230 total_timeout: float = None,
1231 config: dict = None,
quilesj29114342019-10-29 09:30:44 +01001232 ) -> (Application, int):
1233
1234 # get juju model and observer
1235 model = await self._juju_get_model(model_name=model_name)
1236 observer = self.juju_observers[model_name]
1237
1238 # check if application already exists
1239 application = None
1240 if application_name in model.applications:
1241 application = model.applications[application_name]
1242
1243 if application is None:
1244
1245 # application does not exist, create it and wait for it
beierlmf52cb7c2020-04-21 16:36:35 -04001246 self.log.debug(
1247 "deploying application {} to machine {}, model {}".format(
1248 application_name, machine_id, model_name
1249 )
1250 )
1251 self.log.debug("charm: {}".format(charm_path))
David Garciaaf6812a2020-05-25 16:23:20 +02001252 machine = model.machines[machine_id]
quilesjac4e0de2019-11-27 16:12:02 +00001253 # series = None
quilesj29114342019-10-29 09:30:44 +01001254 application = await model.deploy(
1255 entity_url=charm_path,
1256 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001257 channel="stable",
quilesj29114342019-10-29 09:30:44 +01001258 num_units=1,
David Garciaaf6812a2020-05-25 16:23:20 +02001259 series=machine.series,
David Garciadfaa6e82020-04-01 16:06:39 +02001260 to=machine_id,
beierlmf52cb7c2020-04-21 16:36:35 -04001261 config=config,
quilesj29114342019-10-29 09:30:44 +01001262 )
1263
1264 # register application with observer
1265 observer.register_application(application=application, db_dict=db_dict)
1266
beierlmf52cb7c2020-04-21 16:36:35 -04001267 self.log.debug(
1268 "waiting for application deployed... {}".format(application.entity_id)
1269 )
quilesj29114342019-10-29 09:30:44 +01001270 retries = await observer.wait_for_application(
1271 application_id=application.entity_id,
1272 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001273 total_timeout=total_timeout,
1274 )
1275 self.log.debug("application deployed")
quilesj29114342019-10-29 09:30:44 +01001276
1277 else:
1278
1279 # register application with observer
1280 observer.register_application(application=application, db_dict=db_dict)
1281
1282 # application already exists, but not finalised
beierlmf52cb7c2020-04-21 16:36:35 -04001283 self.log.debug("application already exists, waiting for deployed...")
quilesj29114342019-10-29 09:30:44 +01001284 retries = await observer.wait_for_application(
1285 application_id=application.entity_id,
1286 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001287 total_timeout=total_timeout,
1288 )
1289 self.log.debug("application deployed")
quilesj29114342019-10-29 09:30:44 +01001290
1291 return application, retries
1292
1293 async def _juju_execute_action(
beierlmf52cb7c2020-04-21 16:36:35 -04001294 self,
1295 model_name: str,
1296 application_name: str,
1297 action_name: str,
1298 db_dict: dict,
1299 progress_timeout: float = None,
1300 total_timeout: float = None,
1301 **kwargs
quilesj29114342019-10-29 09:30:44 +01001302 ) -> Action:
1303
1304 # get juju model and observer
1305 model = await self._juju_get_model(model_name=model_name)
1306 observer = self.juju_observers[model_name]
1307
beierlmf52cb7c2020-04-21 16:36:35 -04001308 application = await self._juju_get_application(
1309 model_name=model_name, application_name=application_name
1310 )
quilesj29114342019-10-29 09:30:44 +01001311
David Garciaeee9ead2020-03-25 16:23:14 +01001312 unit = None
1313 for u in application.units:
1314 if await u.is_leader_from_status():
1315 unit = u
quilesj29114342019-10-29 09:30:44 +01001316 if unit is not None:
1317 actions = await application.get_actions()
1318 if action_name in actions:
beierlmf52cb7c2020-04-21 16:36:35 -04001319 self.log.debug(
1320 'executing action "{}" using params: {}'.format(action_name, kwargs)
1321 )
quilesj29114342019-10-29 09:30:44 +01001322 action = await unit.run_action(action_name, **kwargs)
1323
1324 # register action with observer
1325 observer.register_action(action=action, db_dict=db_dict)
1326
quilesj29114342019-10-29 09:30:44 +01001327 await observer.wait_for_action(
1328 action_id=action.entity_id,
1329 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001330 total_timeout=total_timeout,
1331 )
1332 self.log.debug("action completed with status: {}".format(action.status))
quilesj29114342019-10-29 09:30:44 +01001333 output = await model.get_action_output(action_uuid=action.entity_id)
1334 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1335 if action.entity_id in status:
1336 status = status[action.entity_id]
1337 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001338 status = "failed"
quilesj29114342019-10-29 09:30:44 +01001339 return output, status
1340
1341 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -04001342 message="Cannot execute action on charm", primitive_name=action_name
quilesj29114342019-10-29 09:30:44 +01001343 )
1344
1345 async def _juju_configure_application(
beierlmf52cb7c2020-04-21 16:36:35 -04001346 self,
1347 model_name: str,
1348 application_name: str,
1349 config: dict,
1350 db_dict: dict,
1351 progress_timeout: float = None,
1352 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +01001353 ):
1354
quilesj29114342019-10-29 09:30:44 +01001355 # get the application
beierlmf52cb7c2020-04-21 16:36:35 -04001356 application = await self._juju_get_application(
1357 model_name=model_name, application_name=application_name
1358 )
quilesj29114342019-10-29 09:30:44 +01001359
beierlmf52cb7c2020-04-21 16:36:35 -04001360 self.log.debug(
1361 "configuring the application {} -> {}".format(application_name, config)
1362 )
quilesj29114342019-10-29 09:30:44 +01001363 res = await application.set_config(config)
beierlmf52cb7c2020-04-21 16:36:35 -04001364 self.log.debug(
1365 "application {} configured. res={}".format(application_name, res)
1366 )
quilesj29114342019-10-29 09:30:44 +01001367
1368 # Verify the config is set
1369 new_conf = await application.get_config()
1370 for key in config:
beierlmf52cb7c2020-04-21 16:36:35 -04001371 value = new_conf[key]["value"]
1372 self.log.debug(" {} = {}".format(key, value))
quilesj29114342019-10-29 09:30:44 +01001373 if config[key] != value:
1374 raise N2VCException(
beierlmf52cb7c2020-04-21 16:36:35 -04001375 message="key {} is not configured correctly {} != {}".format(
1376 key, config[key], new_conf[key]
1377 )
quilesj29114342019-10-29 09:30:44 +01001378 )
1379
1380 # check if 'verify-ssh-credentials' action exists
quilesj073e1692019-11-29 11:19:14 +00001381 # unit = application.units[0]
quilesj29114342019-10-29 09:30:44 +01001382 actions = await application.get_actions()
beierlmf52cb7c2020-04-21 16:36:35 -04001383 if "verify-ssh-credentials" not in actions:
1384 msg = (
1385 "Action verify-ssh-credentials does not exist in application {}"
1386 ).format(application_name)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001387 self.log.debug(msg=msg)
quilesj29114342019-10-29 09:30:44 +01001388 return False
1389
1390 # execute verify-credentials
1391 num_retries = 20
1392 retry_timeout = 15.0
beierlmf52cb7c2020-04-21 16:36:35 -04001393 for _ in range(num_retries):
quilesj29114342019-10-29 09:30:44 +01001394 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001395 self.log.debug("Executing action verify-ssh-credentials...")
quilesj29114342019-10-29 09:30:44 +01001396 output, ok = await self._juju_execute_action(
1397 model_name=model_name,
1398 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001399 action_name="verify-ssh-credentials",
quilesj29114342019-10-29 09:30:44 +01001400 db_dict=db_dict,
1401 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001402 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001403 )
beierlmf52cb7c2020-04-21 16:36:35 -04001404 self.log.debug("Result: {}, output: {}".format(ok, output))
quilesj29114342019-10-29 09:30:44 +01001405 return True
tierno8ff11992020-03-26 09:51:11 +00001406 except asyncio.CancelledError:
1407 raise
quilesj29114342019-10-29 09:30:44 +01001408 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001409 self.log.debug(
1410 "Error executing verify-ssh-credentials: {}. Retrying...".format(e)
1411 )
quilesj29114342019-10-29 09:30:44 +01001412 await asyncio.sleep(retry_timeout)
1413 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001414 self.log.error(
1415 "Error executing verify-ssh-credentials after {} retries. ".format(
1416 num_retries
1417 )
1418 )
quilesj29114342019-10-29 09:30:44 +01001419 return False
1420
beierlmf52cb7c2020-04-21 16:36:35 -04001421 async def _juju_get_application(self, model_name: str, application_name: str):
quilesj29114342019-10-29 09:30:44 +01001422 """Get the deployed application."""
1423
1424 model = await self._juju_get_model(model_name=model_name)
1425
1426 application_name = N2VCJujuConnector._format_app_name(application_name)
1427
1428 if model.applications and application_name in model.applications:
1429 return model.applications[application_name]
1430 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001431 raise N2VCException(
1432 message="Cannot get application {} from model {}".format(
1433 application_name, model_name
1434 )
1435 )
quilesj29114342019-10-29 09:30:44 +01001436
1437 async def _juju_get_model(self, model_name: str) -> Model:
1438 """ Get a model object from juju controller
garciadeblas923510c2019-12-17 15:02:11 +01001439 If the model does not exits, it creates it.
quilesj29114342019-10-29 09:30:44 +01001440
1441 :param str model_name: name of the model
1442 :returns Model: model obtained from juju controller or Exception
1443 """
1444
1445 # format model name
1446 model_name = N2VCJujuConnector._format_model_name(model_name)
1447
1448 if model_name in self.juju_models:
1449 return self.juju_models[model_name]
1450
1451 if self._creating_model:
beierlmf52cb7c2020-04-21 16:36:35 -04001452 self.log.debug("Another coroutine is creating a model. Wait...")
quilesj29114342019-10-29 09:30:44 +01001453 while self._creating_model:
1454 # another coroutine is creating a model, wait
1455 await asyncio.sleep(0.1)
1456 # retry (perhaps another coroutine has created the model meanwhile)
1457 if model_name in self.juju_models:
1458 return self.juju_models[model_name]
1459
1460 try:
1461 self._creating_model = True
1462
1463 # get juju model names from juju
1464 model_list = await self.controller.list_models()
quilesj29114342019-10-29 09:30:44 +01001465 if model_name not in model_list:
beierlmf52cb7c2020-04-21 16:36:35 -04001466 self.log.info(
1467 "Model {} does not exist. Creating new model...".format(model_name)
1468 )
1469 config_dict = {"authorized-keys": self.public_key}
garciadeblas923510c2019-12-17 15:02:11 +01001470 if self.apt_mirror:
beierlmf52cb7c2020-04-21 16:36:35 -04001471 config_dict["apt-mirror"] = self.apt_mirror
garciadeblas923510c2019-12-17 15:02:11 +01001472 if not self.enable_os_upgrade:
David Garciae370f3b2020-04-06 12:42:26 +02001473 config_dict["enable-os-refresh-update"] = False
1474 config_dict["enable-os-upgrade"] = False
David Garcia347aae62020-04-29 12:34:23 +02001475 if self.cloud in self.BUILT_IN_CLOUDS:
1476 model = await self.controller.add_model(
1477 model_name=model_name,
1478 config=config_dict,
1479 cloud_name=self.cloud,
1480 )
1481 else:
1482 model = await self.controller.add_model(
1483 model_name=model_name,
1484 config=config_dict,
1485 cloud_name=self.cloud,
David Garcia32b38122020-05-07 12:28:05 +02001486 credential_name=self.cloud,
David Garcia347aae62020-04-29 12:34:23 +02001487 )
David Garciae370f3b2020-04-06 12:42:26 +02001488 self.log.info("New model created, name={}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001489 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001490 self.log.debug(
1491 "Model already exists in juju. Getting model {}".format(model_name)
1492 )
quilesj29114342019-10-29 09:30:44 +01001493 model = await self.controller.get_model(model_name)
beierlmf52cb7c2020-04-21 16:36:35 -04001494 self.log.debug("Existing model in juju, name={}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001495
1496 self.juju_models[model_name] = model
1497 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1498 return model
1499
1500 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001501 msg = "Cannot get model {}. Exception: {}".format(model_name, e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001502 self.log.error(msg)
quilesj29114342019-10-29 09:30:44 +01001503 raise N2VCException(msg)
1504 finally:
1505 self._creating_model = False
1506
1507 async def _juju_add_relation(
beierlmf52cb7c2020-04-21 16:36:35 -04001508 self,
1509 model_name: str,
1510 application_name_1: str,
1511 application_name_2: str,
1512 relation_1: str,
1513 relation_2: str,
quilesj29114342019-10-29 09:30:44 +01001514 ):
1515
quilesj29114342019-10-29 09:30:44 +01001516 # get juju model and observer
1517 model = await self._juju_get_model(model_name=model_name)
1518
beierlmf52cb7c2020-04-21 16:36:35 -04001519 r1 = "{}:{}".format(application_name_1, relation_1)
1520 r2 = "{}:{}".format(application_name_2, relation_2)
quilesjaae10b42020-01-09 08:49:10 +00001521
beierlmf52cb7c2020-04-21 16:36:35 -04001522 self.log.debug("adding relation: {} -> {}".format(r1, r2))
quilesjaae10b42020-01-09 08:49:10 +00001523 try:
1524 await model.add_relation(relation1=r1, relation2=r2)
1525 except JujuAPIError as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001526 # If one of the applications in the relationship doesn't exist, or the
1527 # relation has already been added,
quilesjaae10b42020-01-09 08:49:10 +00001528 # let the operation fail silently.
beierlmf52cb7c2020-04-21 16:36:35 -04001529 if "not found" in e.message:
quilesjaae10b42020-01-09 08:49:10 +00001530 return
beierlmf52cb7c2020-04-21 16:36:35 -04001531 if "already exists" in e.message:
quilesjaae10b42020-01-09 08:49:10 +00001532 return
1533 # another execption, raise it
1534 raise e
quilesj29114342019-10-29 09:30:44 +01001535
beierlmf52cb7c2020-04-21 16:36:35 -04001536 async def _juju_destroy_application(self, model_name: str, application_name: str):
quilesj29114342019-10-29 09:30:44 +01001537
beierlmf52cb7c2020-04-21 16:36:35 -04001538 self.log.debug(
1539 "Destroying application {} in model {}".format(application_name, model_name)
1540 )
quilesj29114342019-10-29 09:30:44 +01001541
1542 # get juju model and observer
1543 model = await self._juju_get_model(model_name=model_name)
calvinosanch41395222020-02-21 09:25:21 +01001544 observer = self.juju_observers[model_name]
quilesj29114342019-10-29 09:30:44 +01001545
1546 application = model.applications.get(application_name)
1547 if application:
calvinosanch41395222020-02-21 09:25:21 +01001548 observer.unregister_application(application_name)
quilesj29114342019-10-29 09:30:44 +01001549 await application.destroy()
1550 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001551 self.log.debug("Application not found: {}".format(application_name))
quilesj29114342019-10-29 09:30:44 +01001552
1553 async def _juju_destroy_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001554 self, model_name: str, machine_id: str, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +01001555 ):
1556
beierlmf52cb7c2020-04-21 16:36:35 -04001557 self.log.debug(
1558 "Destroying machine {} in model {}".format(machine_id, model_name)
1559 )
quilesj29114342019-10-29 09:30:44 +01001560
1561 if total_timeout is None:
1562 total_timeout = 3600
1563
1564 # get juju model and observer
1565 model = await self._juju_get_model(model_name=model_name)
calvinosanch41395222020-02-21 09:25:21 +01001566 observer = self.juju_observers[model_name]
quilesj29114342019-10-29 09:30:44 +01001567
1568 machines = await model.get_machines()
1569 if machine_id in machines:
1570 machine = model.machines[machine_id]
calvinosanch41395222020-02-21 09:25:21 +01001571 observer.unregister_machine(machine_id)
beierlmf52cb7c2020-04-21 16:36:35 -04001572 # TODO: change this by machine.is_manual when this is upstreamed:
1573 # https://github.com/juju/python-libjuju/pull/396
David Garciadf9f72a2020-04-06 11:02:42 +02001574 if "instance-id" in machine.safe_data and machine.safe_data[
1575 "instance-id"
1576 ].startswith("manual:"):
1577 self.log.debug("machine.destroy(force=True) started.")
1578 await machine.destroy(force=True)
1579 self.log.debug("machine.destroy(force=True) passed.")
1580 # max timeout
1581 end = time.time() + total_timeout
1582 # wait for machine removal
quilesj29114342019-10-29 09:30:44 +01001583 machines = await model.get_machines()
David Garciadf9f72a2020-04-06 11:02:42 +02001584 while machine_id in machines and time.time() < end:
beierlmf52cb7c2020-04-21 16:36:35 -04001585 self.log.debug(
1586 "Waiting for machine {} is destroyed".format(machine_id)
1587 )
David Garciadf9f72a2020-04-06 11:02:42 +02001588 await asyncio.sleep(0.5)
1589 machines = await model.get_machines()
1590 self.log.debug("Machine destroyed: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001591 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001592 self.log.debug("Machine not found: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001593
beierlmf52cb7c2020-04-21 16:36:35 -04001594 async def _juju_destroy_model(self, model_name: str, total_timeout: float = None):
quilesj29114342019-10-29 09:30:44 +01001595
beierlmf52cb7c2020-04-21 16:36:35 -04001596 self.log.debug("Destroying model {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001597
1598 if total_timeout is None:
1599 total_timeout = 3600
tierno8ff11992020-03-26 09:51:11 +00001600 end = time.time() + total_timeout
quilesj29114342019-10-29 09:30:44 +01001601
1602 model = await self._juju_get_model(model_name=model_name)
David Garcia979df142020-04-06 12:55:23 +02001603
1604 if not model:
beierlmf52cb7c2020-04-21 16:36:35 -04001605 raise N2VCNotFound(message="Model {} does not exist".format(model_name))
David Garcia979df142020-04-06 12:55:23 +02001606
quilesj29114342019-10-29 09:30:44 +01001607 uuid = model.info.uuid
1608
David Garciadf9f72a2020-04-06 11:02:42 +02001609 # destroy applications
1610 for application_name in model.applications:
1611 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001612 await self._juju_destroy_application(
1613 model_name=model_name, application_name=application_name
1614 )
David Garciadf9f72a2020-04-06 11:02:42 +02001615 except Exception as e:
1616 self.log.error(
1617 "Error destroying application {} in model {}: {}".format(
beierlmf52cb7c2020-04-21 16:36:35 -04001618 application_name, model_name, e
David Garciadf9f72a2020-04-06 11:02:42 +02001619 )
1620 )
1621
quilesjbd5a0c92020-01-15 12:30:09 +00001622 # destroy machines
1623 machines = await model.get_machines()
1624 for machine_id in machines:
1625 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001626 await self._juju_destroy_machine(
1627 model_name=model_name, machine_id=machine_id
1628 )
tierno8ff11992020-03-26 09:51:11 +00001629 except asyncio.CancelledError:
1630 raise
beierlmf52cb7c2020-04-21 16:36:35 -04001631 except Exception:
quilesjbd5a0c92020-01-15 12:30:09 +00001632 # ignore exceptions destroying machine
1633 pass
1634
quilesj29114342019-10-29 09:30:44 +01001635 await self._juju_disconnect_model(model_name=model_name)
quilesj29114342019-10-29 09:30:44 +01001636
beierlmf52cb7c2020-04-21 16:36:35 -04001637 self.log.debug("destroying model {}...".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001638 await self.controller.destroy_model(uuid)
tierno8ff11992020-03-26 09:51:11 +00001639 # self.log.debug('model destroy requested {}'.format(model_name))
quilesj29114342019-10-29 09:30:44 +01001640
1641 # wait for model is completely destroyed
beierlmf52cb7c2020-04-21 16:36:35 -04001642 self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
1643 last_exception = ""
quilesj29114342019-10-29 09:30:44 +01001644 while time.time() < end:
quilesj29114342019-10-29 09:30:44 +01001645 try:
quilesj776ab392019-12-12 16:10:54 +00001646 # await self.controller.get_model(uuid)
1647 models = await self.controller.list_models()
1648 if model_name not in models:
beierlmf52cb7c2020-04-21 16:36:35 -04001649 self.log.debug(
1650 "The model {} ({}) was destroyed".format(model_name, uuid)
1651 )
quilesj776ab392019-12-12 16:10:54 +00001652 return
tierno8ff11992020-03-26 09:51:11 +00001653 except asyncio.CancelledError:
1654 raise
quilesj776ab392019-12-12 16:10:54 +00001655 except Exception as e:
tierno8ff11992020-03-26 09:51:11 +00001656 last_exception = e
1657 await asyncio.sleep(5)
beierlmf52cb7c2020-04-21 16:36:35 -04001658 raise N2VCException(
1659 "Timeout waiting for model {} to be destroyed {}".format(
1660 model_name, last_exception
1661 )
1662 )
quilesj29114342019-10-29 09:30:44 +01001663
1664 async def _juju_login(self):
1665 """Connect to juju controller
1666
1667 """
1668
1669 # if already authenticated, exit function
1670 if self._authenticated:
1671 return
1672
1673 # if connecting, wait for finish
1674 # another task could be trying to connect in parallel
1675 while self._connecting:
1676 await asyncio.sleep(0.1)
1677
1678 # double check after other task has finished
1679 if self._authenticated:
1680 return
1681
1682 try:
1683 self._connecting = True
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001684 self.log.info(
beierlmf52cb7c2020-04-21 16:36:35 -04001685 "connecting to juju controller: {} {}:{}{}".format(
1686 self.url,
1687 self.username,
1688 self.secret[:8] + "...",
1689 " with ca_cert" if self.ca_cert else "",
1690 )
1691 )
quilesj29114342019-10-29 09:30:44 +01001692
1693 # Create controller object
1694 self.controller = Controller(loop=self.loop)
1695 # Connect to controller
1696 await self.controller.connect(
1697 endpoint=self.url,
1698 username=self.username,
1699 password=self.secret,
beierlmf52cb7c2020-04-21 16:36:35 -04001700 cacert=self.ca_cert,
quilesj29114342019-10-29 09:30:44 +01001701 )
1702 self._authenticated = True
beierlmf52cb7c2020-04-21 16:36:35 -04001703 self.log.info("juju controller connected")
quilesj29114342019-10-29 09:30:44 +01001704 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001705 message = "Exception connecting to juju: {}".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001706 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -04001707 raise N2VCConnectionException(message=message, url=self.url)
quilesj29114342019-10-29 09:30:44 +01001708 finally:
1709 self._connecting = False
1710
1711 async def _juju_logout(self):
1712 """Logout of the Juju controller."""
1713 if not self._authenticated:
1714 return False
1715
1716 # disconnect all models
1717 for model_name in self.juju_models:
1718 try:
1719 await self._juju_disconnect_model(model_name)
1720 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001721 self.log.error(
1722 "Error disconnecting model {} : {}".format(model_name, e)
1723 )
quilesj29114342019-10-29 09:30:44 +01001724 # continue with next model...
1725
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001726 self.log.info("Disconnecting controller")
quilesj29114342019-10-29 09:30:44 +01001727 try:
1728 await self.controller.disconnect()
1729 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001730 raise N2VCConnectionException(
1731 message="Error disconnecting controller: {}".format(e), url=self.url
1732 )
quilesj29114342019-10-29 09:30:44 +01001733
1734 self.controller = None
1735 self._authenticated = False
beierlmf52cb7c2020-04-21 16:36:35 -04001736 self.log.info("disconnected")
quilesj29114342019-10-29 09:30:44 +01001737
beierlmf52cb7c2020-04-21 16:36:35 -04001738 async def _juju_disconnect_model(self, model_name: str):
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001739 self.log.debug("Disconnecting model {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001740 if model_name in self.juju_models:
1741 await self.juju_models[model_name].disconnect()
1742 self.juju_models[model_name] = None
1743 self.juju_observers[model_name] = None
quilesj776ab392019-12-12 16:10:54 +00001744 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001745 self.warning("Cannot disconnect model: {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001746
1747 def _create_juju_public_key(self):
1748 """Recreate the Juju public key on lcm container, if needed
1749 Certain libjuju commands expect to be run from the same machine as Juju
1750 is bootstrapped to. This method will write the public key to disk in
1751 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1752 """
1753
1754 # Make sure that we have a public key before writing to disk
1755 if self.public_key is None or len(self.public_key) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001756 if "OSMLCM_VCA_PUBKEY" in os.environ:
1757 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
quilesj29114342019-10-29 09:30:44 +01001758 if len(self.public_key) == 0:
1759 return
1760 else:
1761 return
1762
beierlmf52cb7c2020-04-21 16:36:35 -04001763 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
quilesj29114342019-10-29 09:30:44 +01001764 file_path = "{}/juju_id_rsa.pub".format(pk_path)
beierlmf52cb7c2020-04-21 16:36:35 -04001765 self.log.debug(
1766 "writing juju public key to file:\n{}\npublic key: {}".format(
1767 file_path, self.public_key
1768 )
1769 )
quilesj29114342019-10-29 09:30:44 +01001770 if not os.path.exists(pk_path):
1771 # create path and write file
1772 os.makedirs(pk_path)
beierlmf52cb7c2020-04-21 16:36:35 -04001773 with open(file_path, "w") as f:
1774 self.log.debug("Creating juju public key file: {}".format(file_path))
quilesj29114342019-10-29 09:30:44 +01001775 f.write(self.public_key)
1776 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001777 self.log.debug("juju public key file already exists: {}".format(file_path))
quilesj29114342019-10-29 09:30:44 +01001778
1779 @staticmethod
1780 def _format_model_name(name: str) -> str:
1781 """Format the name of the model.
1782
1783 Model names may only contain lowercase letters, digits and hyphens
1784 """
1785
beierlmf52cb7c2020-04-21 16:36:35 -04001786 return name.replace("_", "-").replace(" ", "-").lower()
quilesj29114342019-10-29 09:30:44 +01001787
1788 @staticmethod
1789 def _format_app_name(name: str) -> str:
1790 """Format the name of the application (in order to assure valid application name).
1791
1792 Application names have restrictions (run juju deploy --help):
1793 - contains lowercase letters 'a'-'z'
1794 - contains numbers '0'-'9'
1795 - contains hyphens '-'
1796 - starts with a lowercase letter
1797 - not two or more consecutive hyphens
1798 - after a hyphen, not a group with all numbers
1799 """
1800
1801 def all_numbers(s: str) -> bool:
1802 for c in s:
1803 if not c.isdigit():
1804 return False
1805 return True
1806
beierlmf52cb7c2020-04-21 16:36:35 -04001807 new_name = name.replace("_", "-")
1808 new_name = new_name.replace(" ", "-")
quilesj29114342019-10-29 09:30:44 +01001809 new_name = new_name.lower()
beierlmf52cb7c2020-04-21 16:36:35 -04001810 while new_name.find("--") >= 0:
1811 new_name = new_name.replace("--", "-")
1812 groups = new_name.split("-")
quilesj29114342019-10-29 09:30:44 +01001813
1814 # find 'all numbers' groups and prefix them with a letter
beierlmf52cb7c2020-04-21 16:36:35 -04001815 app_name = ""
quilesj29114342019-10-29 09:30:44 +01001816 for i in range(len(groups)):
1817 group = groups[i]
1818 if all_numbers(group):
beierlmf52cb7c2020-04-21 16:36:35 -04001819 group = "z" + group
quilesj29114342019-10-29 09:30:44 +01001820 if i > 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001821 app_name += "-"
quilesj29114342019-10-29 09:30:44 +01001822 app_name += group
1823
1824 if app_name[0].isdigit():
beierlmf52cb7c2020-04-21 16:36:35 -04001825 app_name = "z" + app_name
quilesj29114342019-10-29 09:30:44 +01001826
1827 return app_name