blob: 07c390384c15bd9aca90d8e79732e71ef98f7289 [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
beierlmf52cb7c2020-04-21 16:36:35 -0400169 if "api_proxy" in vca_config:
170 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(
176 "api_proxy is not configured. Support for native charms is disabled"
177 )
quilesj29114342019-10-29 09:30:44 +0100178
beierlmf52cb7c2020-04-21 16:36:35 -0400179 if "enable_os_upgrade" in vca_config:
180 self.enable_os_upgrade = vca_config["enable_os_upgrade"]
garciadeblas923510c2019-12-17 15:02:11 +0100181 else:
182 self.enable_os_upgrade = True
183
beierlmf52cb7c2020-04-21 16:36:35 -0400184 if "apt_mirror" in vca_config:
185 self.apt_mirror = vca_config["apt_mirror"]
garciadeblas923510c2019-12-17 15:02:11 +0100186 else:
187 self.apt_mirror = None
188
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200189 self.cloud = vca_config.get('cloud')
190 self.k8s_cloud = None
191 if "k8s_cloud" in vca_config:
192 self.k8s_cloud = vca_config.get("k8s_cloud")
193 self.log.debug('Arguments have been checked')
quilesj29114342019-10-29 09:30:44 +0100194
195 # juju data
beierlmf52cb7c2020-04-21 16:36:35 -0400196 self.controller = None # it will be filled when connect to juju
197 self.juju_models = {} # model objects for every model_name
198 self.juju_observers = {} # model observers for every model_name
199 self._connecting = (
200 False # while connecting to juju (to avoid duplicate connections)
201 )
202 self._authenticated = (
203 False # it will be True when juju connection be stablished
204 )
205 self._creating_model = False # True during model creation
David Garcia4fee80e2020-05-13 12:18:38 +0200206 self.libjuju = Libjuju(
207 endpoint=self.url,
208 api_proxy=self.api_proxy,
209 enable_os_upgrade=self.enable_os_upgrade,
210 apt_mirror=self.apt_mirror,
211 username=self.username,
212 password=self.secret,
213 cacert=self.ca_cert,
214 loop=self.loop,
215 log=self.log,
216 db=self.db,
217 n2vc=self,
218 )
quilesj29114342019-10-29 09:30:44 +0100219
beierlmf52cb7c2020-04-21 16:36:35 -0400220 # create juju pub key file in lcm container at
221 # ./local/share/juju/ssh/juju_id_rsa.pub
quilesj29114342019-10-29 09:30:44 +0100222 self._create_juju_public_key()
223
beierlmf52cb7c2020-04-21 16:36:35 -0400224 self.log.info("N2VC juju connector initialized")
quilesj29114342019-10-29 09:30:44 +0100225
quilesj776ab392019-12-12 16:10:54 +0000226 async def get_status(self, namespace: str, yaml_format: bool = True):
227
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100228 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
quilesj29114342019-10-29 09:30:44 +0100229
beierlmf52cb7c2020-04-21 16:36:35 -0400230 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
231 namespace=namespace
232 )
quilesj29114342019-10-29 09:30:44 +0100233 # model name is ns_id
234 model_name = ns_id
235 if model_name is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400236 msg = "Namespace {} not valid".format(namespace)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100237 self.log.error(msg)
beierlmf52cb7c2020-04-21 16:36:35 -0400238 raise N2VCBadArgumentsException(msg, ["namespace"])
quilesj29114342019-10-29 09:30:44 +0100239
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200240 status = {}
241 models = await self.libjuju.list_models(contains=ns_id)
242
243 for m in models:
244 status[m] = self.libjuju.get_model_status(m)
quilesj29114342019-10-29 09:30:44 +0100245
quilesj776ab392019-12-12 16:10:54 +0000246 if yaml_format:
247 return obj_to_yaml(status)
248 else:
249 return obj_to_dict(status)
quilesj29114342019-10-29 09:30:44 +0100250
251 async def create_execution_environment(
252 self,
253 namespace: str,
254 db_dict: dict,
255 reuse_ee_id: str = None,
256 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400257 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100258 ) -> (str, dict):
259
beierlmf52cb7c2020-04-21 16:36:35 -0400260 self.log.info(
261 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
262 namespace, reuse_ee_id
263 )
264 )
quilesj29114342019-10-29 09:30:44 +0100265
quilesj29114342019-10-29 09:30:44 +0100266 machine_id = None
267 if reuse_ee_id:
beierlmf52cb7c2020-04-21 16:36:35 -0400268 model_name, application_name, machine_id = self._get_ee_id_components(
269 ee_id=reuse_ee_id
270 )
quilesj29114342019-10-29 09:30:44 +0100271 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400272 (
273 _nsi_id,
274 ns_id,
275 _vnf_id,
276 _vdu_id,
277 _vdu_count,
278 ) = self._get_namespace_components(namespace=namespace)
quilesj29114342019-10-29 09:30:44 +0100279 # model name is ns_id
280 model_name = ns_id
281 # application name
282 application_name = self._get_application_name(namespace=namespace)
283
beierlmf52cb7c2020-04-21 16:36:35 -0400284 self.log.debug(
285 "model name: {}, application name: {}, machine_id: {}".format(
286 model_name, application_name, machine_id
287 )
288 )
quilesj29114342019-10-29 09:30:44 +0100289
290 # create or reuse a new juju machine
291 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200292 if not await self.libjuju.model_exists(model_name):
293 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
294 machine, new = await self.libjuju.create_machine(
quilesj29114342019-10-29 09:30:44 +0100295 model_name=model_name,
quilesj29114342019-10-29 09:30:44 +0100296 machine_id=machine_id,
297 db_dict=db_dict,
298 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400299 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100300 )
David Garcia4fee80e2020-05-13 12:18:38 +0200301 # id for the execution environment
302 ee_id = N2VCJujuConnector._build_ee_id(
303 model_name=model_name,
304 application_name=application_name,
305 machine_id=str(machine.entity_id),
306 )
307 self.log.debug("ee_id: {}".format(ee_id))
308
309 if new:
310 # write ee_id in database
311 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
312
quilesj29114342019-10-29 09:30:44 +0100313 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400314 message = "Error creating machine on juju: {}".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100315 self.log.error(message)
quilesj29114342019-10-29 09:30:44 +0100316 raise N2VCException(message=message)
317
quilesj29114342019-10-29 09:30:44 +0100318 # new machine credentials
David Garcia4fee80e2020-05-13 12:18:38 +0200319 credentials = {
320 "hostname": machine.dns_name,
321 }
quilesj29114342019-10-29 09:30:44 +0100322
beierlmf52cb7c2020-04-21 16:36:35 -0400323 self.log.info(
324 "Execution environment created. ee_id: {}, credentials: {}".format(
325 ee_id, credentials
326 )
327 )
quilesj29114342019-10-29 09:30:44 +0100328
329 return ee_id, credentials
330
331 async def register_execution_environment(
332 self,
333 namespace: str,
334 credentials: dict,
335 db_dict: dict,
336 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400337 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100338 ) -> str:
339
beierlmf52cb7c2020-04-21 16:36:35 -0400340 self.log.info(
341 "Registering execution environment. namespace={}, credentials={}".format(
342 namespace, credentials
343 )
344 )
quilesj29114342019-10-29 09:30:44 +0100345
346 if credentials is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400347 raise N2VCBadArgumentsException(
348 message="credentials are mandatory", bad_args=["credentials"]
349 )
350 if credentials.get("hostname"):
351 hostname = credentials["hostname"]
quilesj29114342019-10-29 09:30:44 +0100352 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400353 raise N2VCBadArgumentsException(
354 message="hostname is mandatory", bad_args=["credentials.hostname"]
355 )
356 if credentials.get("username"):
357 username = credentials["username"]
quilesj29114342019-10-29 09:30:44 +0100358 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400359 raise N2VCBadArgumentsException(
360 message="username is mandatory", bad_args=["credentials.username"]
361 )
362 if "private_key_path" in credentials:
363 private_key_path = credentials["private_key_path"]
quilesj29114342019-10-29 09:30:44 +0100364 else:
365 # if not passed as argument, use generated private key path
366 private_key_path = self.private_key_path
367
beierlmf52cb7c2020-04-21 16:36:35 -0400368 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
369 namespace=namespace
370 )
quilesj29114342019-10-29 09:30:44 +0100371
372 # model name
373 model_name = ns_id
374 # application name
375 application_name = self._get_application_name(namespace=namespace)
376
377 # register machine on juju
378 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200379 if not self.api_proxy:
380 msg = "Cannot provision machine: api_proxy is not defined"
381 self.log.error(msg=msg)
382 raise N2VCException(message=msg)
383 if not await self.libjuju.model_exists(model_name):
384 await self.libjuju.add_model(model_name, cloud_name=self.cloud)
385 machine_id = await self.libjuju.provision_machine(
quilesj29114342019-10-29 09:30:44 +0100386 model_name=model_name,
387 hostname=hostname,
388 username=username,
389 private_key_path=private_key_path,
390 db_dict=db_dict,
391 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400392 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100393 )
394 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400395 self.log.error("Error registering machine: {}".format(e))
396 raise N2VCException(
397 message="Error registering machine on juju: {}".format(e)
398 )
quilesjac4e0de2019-11-27 16:12:02 +0000399
beierlmf52cb7c2020-04-21 16:36:35 -0400400 self.log.info("Machine registered: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +0100401
402 # id for the execution environment
403 ee_id = N2VCJujuConnector._build_ee_id(
404 model_name=model_name,
405 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400406 machine_id=str(machine_id),
quilesj29114342019-10-29 09:30:44 +0100407 )
408
beierlmf52cb7c2020-04-21 16:36:35 -0400409 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100410
411 return ee_id
412
413 async def install_configuration_sw(
414 self,
415 ee_id: str,
416 artifact_path: str,
417 db_dict: dict,
418 progress_timeout: float = None,
David Garciadfaa6e82020-04-01 16:06:39 +0200419 total_timeout: float = None,
420 config: dict = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100421 num_units: int = 1,
quilesj29114342019-10-29 09:30:44 +0100422 ):
423
beierlmf52cb7c2020-04-21 16:36:35 -0400424 self.log.info(
425 (
426 "Installing configuration sw on ee_id: {}, "
427 "artifact path: {}, db_dict: {}"
428 ).format(ee_id, artifact_path, db_dict)
429 )
quilesj29114342019-10-29 09:30:44 +0100430
quilesj29114342019-10-29 09:30:44 +0100431 # check arguments
432 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400433 raise N2VCBadArgumentsException(
434 message="ee_id is mandatory", bad_args=["ee_id"]
435 )
quilesj29114342019-10-29 09:30:44 +0100436 if artifact_path is None or len(artifact_path) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400437 raise N2VCBadArgumentsException(
438 message="artifact_path is mandatory", bad_args=["artifact_path"]
439 )
quilesj29114342019-10-29 09:30:44 +0100440 if db_dict is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400441 raise N2VCBadArgumentsException(
442 message="db_dict is mandatory", bad_args=["db_dict"]
443 )
quilesj29114342019-10-29 09:30:44 +0100444
445 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400446 (
447 model_name,
448 application_name,
449 machine_id,
450 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
451 self.log.debug(
452 "model: {}, application: {}, machine: {}".format(
453 model_name, application_name, machine_id
454 )
455 )
456 except Exception:
quilesj29114342019-10-29 09:30:44 +0100457 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400458 message="ee_id={} is not a valid execution environment id".format(
459 ee_id
460 ),
461 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100462 )
463
464 # remove // in charm path
beierlmf52cb7c2020-04-21 16:36:35 -0400465 while artifact_path.find("//") >= 0:
466 artifact_path = artifact_path.replace("//", "/")
quilesj29114342019-10-29 09:30:44 +0100467
468 # check charm path
469 if not self.fs.file_exists(artifact_path, mode="dir"):
beierlmf52cb7c2020-04-21 16:36:35 -0400470 msg = "artifact path does not exist: {}".format(artifact_path)
471 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
quilesj29114342019-10-29 09:30:44 +0100472
beierlmf52cb7c2020-04-21 16:36:35 -0400473 if artifact_path.startswith("/"):
quilesj29114342019-10-29 09:30:44 +0100474 full_path = self.fs.path + artifact_path
475 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400476 full_path = self.fs.path + "/" + artifact_path
quilesj29114342019-10-29 09:30:44 +0100477
478 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200479 await self.libjuju.deploy_charm(
quilesj29114342019-10-29 09:30:44 +0100480 model_name=model_name,
481 application_name=application_name,
David Garcia4fee80e2020-05-13 12:18:38 +0200482 path=full_path,
quilesj29114342019-10-29 09:30:44 +0100483 machine_id=machine_id,
484 db_dict=db_dict,
485 progress_timeout=progress_timeout,
David Garciadfaa6e82020-04-01 16:06:39 +0200486 total_timeout=total_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400487 config=config,
David Garciaf8a9d462020-03-25 18:19:02 +0100488 num_units=num_units,
quilesj29114342019-10-29 09:30:44 +0100489 )
490 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400491 raise N2VCException(
492 message="Error desploying charm into ee={} : {}".format(ee_id, e)
493 )
quilesj29114342019-10-29 09:30:44 +0100494
beierlmf52cb7c2020-04-21 16:36:35 -0400495 self.log.info("Configuration sw installed")
quilesj29114342019-10-29 09:30:44 +0100496
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200497 async def install_k8s_proxy_charm(
498 self,
499 charm_name: str,
500 namespace: str,
501 artifact_path: str,
502 db_dict: dict,
503 progress_timeout: float = None,
504 total_timeout: float = None,
505 config: dict = None,
506 ) -> str:
507 """
508 Install a k8s proxy charm
509
510 :param charm_name: Name of the charm being deployed
511 :param namespace: collection of all the uuids related to the charm.
512 :param str artifact_path: where to locate the artifacts (parent folder) using
513 the self.fs
514 the final artifact path will be a combination of this artifact_path and
515 additional string from the config_dict (e.g. charm name)
516 :param dict db_dict: where to write into database when the status changes.
517 It contains a dict with
518 {collection: <str>, filter: {}, path: <str>},
519 e.g. {collection: "nsrs", filter:
520 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
521 :param float progress_timeout:
522 :param float total_timeout:
523 :param config: Dictionary with additional configuration
524
525 :returns ee_id: execution environment id.
526 """
527 self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
528 .format(charm_name, artifact_path, db_dict))
529
530 if not self.k8s_cloud:
531 raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
532
533 if artifact_path is None or len(artifact_path) == 0:
534 raise N2VCBadArgumentsException(
535 message="artifact_path is mandatory", bad_args=["artifact_path"]
536 )
537 if db_dict is None:
538 raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
539
540 # remove // in charm path
541 while artifact_path.find('//') >= 0:
542 artifact_path = artifact_path.replace('//', '/')
543
544 # check charm path
545 if not self.fs.file_exists(artifact_path, mode="dir"):
546 msg = 'artifact path does not exist: {}'.format(artifact_path)
547 raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
548
549 if artifact_path.startswith('/'):
550 full_path = self.fs.path + artifact_path
551 else:
552 full_path = self.fs.path + '/' + artifact_path
553
554 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
555 model_name = '{}-k8s'.format(ns_id)
556
557 await self.libjuju.add_model(model_name, self.k8s_cloud)
558 application_name = self._get_application_name(namespace)
559
560 try:
561 await self.libjuju.deploy_charm(
562 model_name=model_name,
563 application_name=application_name,
564 path=full_path,
565 machine_id=None,
566 db_dict=db_dict,
567 progress_timeout=progress_timeout,
568 total_timeout=total_timeout,
569 config=config
570 )
571 except Exception as e:
572 raise N2VCException(message='Error deploying charm: {}'.format(e))
573
574 self.log.info('K8s proxy charm installed')
575 ee_id = N2VCJujuConnector._build_ee_id(
576 model_name=model_name,
577 application_name=application_name,
578 machine_id="k8s",
579 )
Dominik Fleischmann7ace6fa2020-06-29 16:16:28 +0200580
581 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
582
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200583 return ee_id
584
quilesj29114342019-10-29 09:30:44 +0100585 async def get_ee_ssh_public__key(
586 self,
587 ee_id: str,
588 db_dict: dict,
589 progress_timeout: float = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400590 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100591 ) -> str:
592
beierlmf52cb7c2020-04-21 16:36:35 -0400593 self.log.info(
594 (
595 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
596 ).format(ee_id, db_dict)
597 )
quilesj29114342019-10-29 09:30:44 +0100598
quilesj29114342019-10-29 09:30:44 +0100599 # check arguments
600 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400601 raise N2VCBadArgumentsException(
602 message="ee_id is mandatory", bad_args=["ee_id"]
603 )
quilesj29114342019-10-29 09:30:44 +0100604 if db_dict is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400605 raise N2VCBadArgumentsException(
606 message="db_dict is mandatory", bad_args=["db_dict"]
607 )
quilesj29114342019-10-29 09:30:44 +0100608
609 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400610 (
611 model_name,
612 application_name,
613 machine_id,
614 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
615 self.log.debug(
616 "model: {}, application: {}, machine: {}".format(
617 model_name, application_name, machine_id
618 )
619 )
620 except Exception:
quilesj29114342019-10-29 09:30:44 +0100621 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400622 message="ee_id={} is not a valid execution environment id".format(
623 ee_id
624 ),
625 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100626 )
627
628 # try to execute ssh layer primitives (if exist):
629 # generate-ssh-key
630 # get-ssh-public-key
631
632 output = None
633
David Garcia4fee80e2020-05-13 12:18:38 +0200634 application_name = N2VCJujuConnector._format_app_name(application_name)
635
quilesj29114342019-10-29 09:30:44 +0100636 # execute action: generate-ssh-key
637 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200638 output, _status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100639 model_name=model_name,
640 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400641 action_name="generate-ssh-key",
quilesj29114342019-10-29 09:30:44 +0100642 db_dict=db_dict,
643 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400644 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100645 )
646 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400647 self.log.info(
648 "Skipping exception while executing action generate-ssh-key: {}".format(
649 e
650 )
651 )
quilesj29114342019-10-29 09:30:44 +0100652
653 # execute action: get-ssh-public-key
654 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200655 output, _status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100656 model_name=model_name,
657 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -0400658 action_name="get-ssh-public-key",
quilesj29114342019-10-29 09:30:44 +0100659 db_dict=db_dict,
660 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -0400661 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +0100662 )
663 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400664 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100665 self.log.info(msg)
David Garcia4fee80e2020-05-13 12:18:38 +0200666 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key")
quilesj29114342019-10-29 09:30:44 +0100667
668 # return public key if exists
David Garcia9ae8fa52019-12-09 18:50:03 +0100669 return output["pubkey"] if "pubkey" in output else output
quilesj29114342019-10-29 09:30:44 +0100670
671 async def add_relation(
beierlmf52cb7c2020-04-21 16:36:35 -0400672 self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
quilesj29114342019-10-29 09:30:44 +0100673 ):
674
beierlmf52cb7c2020-04-21 16:36:35 -0400675 self.log.debug(
676 "adding new relation between {} and {}, endpoints: {}, {}".format(
677 ee_id_1, ee_id_2, endpoint_1, endpoint_2
678 )
679 )
quilesj29114342019-10-29 09:30:44 +0100680
quilesjaae10b42020-01-09 08:49:10 +0000681 # check arguments
682 if not ee_id_1:
beierlmf52cb7c2020-04-21 16:36:35 -0400683 message = "EE 1 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100684 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400685 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
quilesjaae10b42020-01-09 08:49:10 +0000686 if not ee_id_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400687 message = "EE 2 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100688 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400689 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
quilesjaae10b42020-01-09 08:49:10 +0000690 if not endpoint_1:
beierlmf52cb7c2020-04-21 16:36:35 -0400691 message = "endpoint 1 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100692 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400693 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
quilesjaae10b42020-01-09 08:49:10 +0000694 if not endpoint_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400695 message = "endpoint 2 is mandatory"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100696 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400697 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
quilesjaae10b42020-01-09 08:49:10 +0000698
quilesjaae10b42020-01-09 08:49:10 +0000699 # get the model, the applications and the machines from the ee_id's
beierlmf52cb7c2020-04-21 16:36:35 -0400700 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
701 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
quilesj29114342019-10-29 09:30:44 +0100702
703 # model must be the same
704 if model_1 != model_2:
beierlmf52cb7c2020-04-21 16:36:35 -0400705 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100706 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -0400707 raise N2VCBadArgumentsException(
708 message=message, bad_args=["ee_id_1", "ee_id_2"]
709 )
quilesj29114342019-10-29 09:30:44 +0100710
711 # add juju relations between two applications
712 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200713 await self.libjuju.add_relation(
quilesjaae10b42020-01-09 08:49:10 +0000714 model_name=model_1,
715 application_name_1=app_1,
716 application_name_2=app_2,
717 relation_1=endpoint_1,
beierlmf52cb7c2020-04-21 16:36:35 -0400718 relation_2=endpoint_2,
quilesjaae10b42020-01-09 08:49:10 +0000719 )
quilesj29114342019-10-29 09:30:44 +0100720 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400721 message = "Error adding relation between {} and {}: {}".format(
722 ee_id_1, ee_id_2, e
723 )
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100724 self.log.error(message)
quilesj29114342019-10-29 09:30:44 +0100725 raise N2VCException(message=message)
726
beierlmf52cb7c2020-04-21 16:36:35 -0400727 async def remove_relation(self):
quilesj29114342019-10-29 09:30:44 +0100728 # TODO
beierlmf52cb7c2020-04-21 16:36:35 -0400729 self.log.info("Method not implemented yet")
730 raise MethodNotImplemented()
quilesj29114342019-10-29 09:30:44 +0100731
beierlmf52cb7c2020-04-21 16:36:35 -0400732 async def deregister_execution_environments(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400733 self.log.info("Method not implemented yet")
734 raise MethodNotImplemented()
quilesj29114342019-10-29 09:30:44 +0100735
736 async def delete_namespace(
beierlmf52cb7c2020-04-21 16:36:35 -0400737 self, namespace: str, db_dict: dict = None, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +0100738 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400739 self.log.info("Deleting namespace={}".format(namespace))
quilesj29114342019-10-29 09:30:44 +0100740
quilesj29114342019-10-29 09:30:44 +0100741 # check arguments
742 if namespace is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400743 raise N2VCBadArgumentsException(
744 message="namespace is mandatory", bad_args=["namespace"]
745 )
quilesj29114342019-10-29 09:30:44 +0100746
beierlmf52cb7c2020-04-21 16:36:35 -0400747 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
748 namespace=namespace
749 )
quilesj29114342019-10-29 09:30:44 +0100750 if ns_id is not None:
quilesj29114342019-10-29 09:30:44 +0100751 try:
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200752 models = await self.libjuju.list_models(contains=ns_id)
753 for model in models:
754 await self.libjuju.destroy_model(
755 model_name=model, total_timeout=total_timeout
756 )
quilesj29114342019-10-29 09:30:44 +0100757 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400758 raise N2VCException(
759 message="Error deleting namespace {} : {}".format(namespace, e)
760 )
quilesj29114342019-10-29 09:30:44 +0100761 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400762 raise N2VCBadArgumentsException(
763 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
764 )
quilesj29114342019-10-29 09:30:44 +0100765
beierlmf52cb7c2020-04-21 16:36:35 -0400766 self.log.info("Namespace {} deleted".format(namespace))
quilesj29114342019-10-29 09:30:44 +0100767
768 async def delete_execution_environment(
beierlmf52cb7c2020-04-21 16:36:35 -0400769 self, ee_id: str, db_dict: dict = None, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +0100770 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400771 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100772
quilesj29114342019-10-29 09:30:44 +0100773 # check arguments
774 if ee_id is None:
beierlmf52cb7c2020-04-21 16:36:35 -0400775 raise N2VCBadArgumentsException(
776 message="ee_id is mandatory", bad_args=["ee_id"]
777 )
quilesj29114342019-10-29 09:30:44 +0100778
beierlmf52cb7c2020-04-21 16:36:35 -0400779 model_name, application_name, _machine_id = self._get_ee_id_components(
780 ee_id=ee_id
781 )
quilesj29114342019-10-29 09:30:44 +0100782
783 # destroy the application
784 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200785 await self.libjuju.destroy_model(
786 model_name=model_name, total_timeout=total_timeout
beierlmf52cb7c2020-04-21 16:36:35 -0400787 )
quilesj29114342019-10-29 09:30:44 +0100788 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400789 raise N2VCException(
790 message=(
791 "Error deleting execution environment {} (application {}) : {}"
792 ).format(ee_id, application_name, e)
793 )
quilesj29114342019-10-29 09:30:44 +0100794
795 # destroy the machine
beierlmf52cb7c2020-04-21 16:36:35 -0400796 # try:
calvinosanch41395222020-02-21 09:25:21 +0100797 # await self._juju_destroy_machine(
798 # model_name=model_name,
799 # machine_id=machine_id,
800 # total_timeout=total_timeout
801 # )
802 # except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400803 # raise N2VCException(
804 # message='Error deleting execution environment {} (machine {}) : {}'
805 # .format(ee_id, machine_id, e))
quilesj29114342019-10-29 09:30:44 +0100806
beierlmf52cb7c2020-04-21 16:36:35 -0400807 self.log.info("Execution environment {} deleted".format(ee_id))
quilesj29114342019-10-29 09:30:44 +0100808
809 async def exec_primitive(
beierlmf52cb7c2020-04-21 16:36:35 -0400810 self,
811 ee_id: str,
812 primitive_name: str,
813 params_dict: dict,
814 db_dict: dict = None,
815 progress_timeout: float = None,
816 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +0100817 ) -> str:
818
beierlmf52cb7c2020-04-21 16:36:35 -0400819 self.log.info(
820 "Executing primitive: {} on ee: {}, params: {}".format(
821 primitive_name, ee_id, params_dict
822 )
823 )
quilesj29114342019-10-29 09:30:44 +0100824
quilesj29114342019-10-29 09:30:44 +0100825 # check arguments
826 if ee_id is None or len(ee_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400827 raise N2VCBadArgumentsException(
828 message="ee_id is mandatory", bad_args=["ee_id"]
829 )
quilesj29114342019-10-29 09:30:44 +0100830 if primitive_name is None or len(primitive_name) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -0400831 raise N2VCBadArgumentsException(
832 message="action_name is mandatory", bad_args=["action_name"]
833 )
quilesj29114342019-10-29 09:30:44 +0100834 if params_dict is None:
835 params_dict = dict()
836
837 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400838 (
839 model_name,
840 application_name,
841 _machine_id,
842 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
quilesj29114342019-10-29 09:30:44 +0100843 except Exception:
844 raise N2VCBadArgumentsException(
beierlmf52cb7c2020-04-21 16:36:35 -0400845 message="ee_id={} is not a valid execution environment id".format(
846 ee_id
847 ),
848 bad_args=["ee_id"],
quilesj29114342019-10-29 09:30:44 +0100849 )
850
beierlmf52cb7c2020-04-21 16:36:35 -0400851 if primitive_name == "config":
quilesj29114342019-10-29 09:30:44 +0100852 # Special case: config primitive
853 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200854 await self.libjuju.configure_application(
quilesj29114342019-10-29 09:30:44 +0100855 model_name=model_name,
856 application_name=application_name,
857 config=params_dict,
quilesj29114342019-10-29 09:30:44 +0100858 )
David Garcia4fee80e2020-05-13 12:18:38 +0200859 actions = await self.libjuju.get_actions(
860 application_name=application_name, model_name=model_name,
861 )
862 self.log.debug(
863 "Application {} has these actions: {}".format(
864 application_name, actions
865 )
866 )
867 if "verify-ssh-credentials" in actions:
868 # execute verify-credentials
869 num_retries = 20
870 retry_timeout = 15.0
871 for _ in range(num_retries):
872 try:
873 self.log.debug("Executing action verify-ssh-credentials...")
874 output, ok = await self.libjuju.execute_action(
875 model_name=model_name,
876 application_name=application_name,
877 action_name="verify-ssh-credentials",
878 db_dict=db_dict,
879 progress_timeout=progress_timeout,
880 total_timeout=total_timeout,
881 )
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200882
883 if ok == "failed":
884 self.log.debug(
885 "Error executing verify-ssh-credentials: {}. Retrying..."
886 )
887 await asyncio.sleep(retry_timeout)
888
889 continue
David Garcia4fee80e2020-05-13 12:18:38 +0200890 self.log.debug("Result: {}, output: {}".format(ok, output))
891 break
892 except asyncio.CancelledError:
893 raise
David Garcia4fee80e2020-05-13 12:18:38 +0200894 else:
895 self.log.error(
896 "Error executing verify-ssh-credentials after {} retries. ".format(
897 num_retries
898 )
899 )
900 else:
901 msg = "Action verify-ssh-credentials does not exist in application {}".format(
902 application_name
903 )
904 self.log.debug(msg=msg)
quilesj29114342019-10-29 09:30:44 +0100905 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400906 self.log.error("Error configuring juju application: {}".format(e))
quilesj29114342019-10-29 09:30:44 +0100907 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -0400908 message="Error configuring application into ee={} : {}".format(
909 ee_id, e
910 ),
911 primitive_name=primitive_name,
quilesj29114342019-10-29 09:30:44 +0100912 )
beierlmf52cb7c2020-04-21 16:36:35 -0400913 return "CONFIG OK"
quilesj29114342019-10-29 09:30:44 +0100914 else:
915 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200916 output, status = await self.libjuju.execute_action(
quilesj29114342019-10-29 09:30:44 +0100917 model_name=model_name,
918 application_name=application_name,
919 action_name=primitive_name,
920 db_dict=db_dict,
921 progress_timeout=progress_timeout,
922 total_timeout=total_timeout,
923 **params_dict
924 )
beierlmf52cb7c2020-04-21 16:36:35 -0400925 if status == "completed":
quilesj29114342019-10-29 09:30:44 +0100926 return output
927 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400928 raise Exception("status is not completed: {}".format(status))
quilesj29114342019-10-29 09:30:44 +0100929 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400930 self.log.error(
931 "Error executing primitive {}: {}".format(primitive_name, e)
932 )
quilesj29114342019-10-29 09:30:44 +0100933 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -0400934 message="Error executing primitive {} into ee={} : {}".format(
935 primitive_name, ee_id, e
936 ),
937 primitive_name=primitive_name,
quilesj29114342019-10-29 09:30:44 +0100938 )
939
940 async def disconnect(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400941 self.log.info("closing juju N2VC...")
David Garcia4fee80e2020-05-13 12:18:38 +0200942 try:
943 await self.libjuju.disconnect()
944 except Exception as e:
945 raise N2VCConnectionException(
946 message="Error disconnecting controller: {}".format(e), url=self.url
947 )
quilesj29114342019-10-29 09:30:44 +0100948
949 """
David Garcia4fee80e2020-05-13 12:18:38 +0200950####################################################################################
951################################### P R I V A T E ##################################
952####################################################################################
quilesj29114342019-10-29 09:30:44 +0100953 """
954
beierlmf52cb7c2020-04-21 16:36:35 -0400955 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
quilesj29114342019-10-29 09:30:44 +0100956
957 # write ee_id to database: _admin.deployed.VCA.x
958 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400959 the_table = db_dict["collection"]
960 the_filter = db_dict["filter"]
961 the_path = db_dict["path"]
962 if not the_path[-1] == ".":
963 the_path = the_path + "."
964 update_dict = {the_path + "ee_id": ee_id}
Dominik Fleischmannf9bed352020-02-27 10:04:34 +0100965 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
quilesj29114342019-10-29 09:30:44 +0100966 self.db.set_one(
967 table=the_table,
968 q_filter=the_filter,
969 update_dict=update_dict,
beierlmf52cb7c2020-04-21 16:36:35 -0400970 fail_on_empty=True,
quilesj29114342019-10-29 09:30:44 +0100971 )
tierno8ff11992020-03-26 09:51:11 +0000972 except asyncio.CancelledError:
973 raise
quilesj29114342019-10-29 09:30:44 +0100974 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -0400975 self.log.error("Error writing ee_id to database: {}".format(e))
quilesj29114342019-10-29 09:30:44 +0100976
977 @staticmethod
beierlmf52cb7c2020-04-21 16:36:35 -0400978 def _build_ee_id(model_name: str, application_name: str, machine_id: str):
quilesj29114342019-10-29 09:30:44 +0100979 """
980 Build an execution environment id form model, application and machine
981 :param model_name:
982 :param application_name:
983 :param machine_id:
984 :return:
985 """
986 # id for the execution environment
beierlmf52cb7c2020-04-21 16:36:35 -0400987 return "{}.{}.{}".format(model_name, application_name, machine_id)
quilesj29114342019-10-29 09:30:44 +0100988
989 @staticmethod
beierlmf52cb7c2020-04-21 16:36:35 -0400990 def _get_ee_id_components(ee_id: str) -> (str, str, str):
quilesj29114342019-10-29 09:30:44 +0100991 """
992 Get model, application and machine components from an execution environment id
993 :param ee_id:
994 :return: model_name, application_name, machine_id
995 """
996
997 if ee_id is None:
998 return None, None, None
999
1000 # split components of id
beierlmf52cb7c2020-04-21 16:36:35 -04001001 parts = ee_id.split(".")
quilesj29114342019-10-29 09:30:44 +01001002 model_name = parts[0]
1003 application_name = parts[1]
1004 machine_id = parts[2]
1005 return model_name, application_name, machine_id
1006
1007 def _get_application_name(self, namespace: str) -> str:
1008 """
1009 Build application name from namespace
1010 :param namespace:
1011 :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
1012 """
1013
Adam Israel18046072019-12-08 21:44:29 -05001014 # TODO: Enforce the Juju 50-character application limit
1015
quilesj29114342019-10-29 09:30:44 +01001016 # split namespace components
beierlmf52cb7c2020-04-21 16:36:35 -04001017 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
1018 namespace=namespace
1019 )
quilesj29114342019-10-29 09:30:44 +01001020
1021 if vnf_id is None or len(vnf_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001022 vnf_id = ""
quilesj29114342019-10-29 09:30:44 +01001023 else:
Adam Israel18046072019-12-08 21:44:29 -05001024 # Shorten the vnf_id to its last twelve characters
beierlmf52cb7c2020-04-21 16:36:35 -04001025 vnf_id = "vnf-" + vnf_id[-12:]
quilesj29114342019-10-29 09:30:44 +01001026
1027 if vdu_id is None or len(vdu_id) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001028 vdu_id = ""
quilesj29114342019-10-29 09:30:44 +01001029 else:
Adam Israel18046072019-12-08 21:44:29 -05001030 # Shorten the vdu_id to its last twelve characters
beierlmf52cb7c2020-04-21 16:36:35 -04001031 vdu_id = "-vdu-" + vdu_id[-12:]
quilesj29114342019-10-29 09:30:44 +01001032
1033 if vdu_count is None or len(vdu_count) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001034 vdu_count = ""
quilesj29114342019-10-29 09:30:44 +01001035 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001036 vdu_count = "-cnt-" + vdu_count
quilesj29114342019-10-29 09:30:44 +01001037
beierlmf52cb7c2020-04-21 16:36:35 -04001038 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
quilesj29114342019-10-29 09:30:44 +01001039
1040 return N2VCJujuConnector._format_app_name(application_name)
1041
1042 async def _juju_create_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001043 self,
1044 model_name: str,
1045 application_name: str,
1046 machine_id: str = None,
1047 db_dict: dict = None,
1048 progress_timeout: float = None,
1049 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +01001050 ) -> Machine:
1051
beierlmf52cb7c2020-04-21 16:36:35 -04001052 self.log.debug(
1053 "creating machine in model: {}, existing machine id: {}".format(
1054 model_name, machine_id
1055 )
1056 )
quilesj29114342019-10-29 09:30:44 +01001057
1058 # get juju model and observer (create model if needed)
1059 model = await self._juju_get_model(model_name=model_name)
1060 observer = self.juju_observers[model_name]
1061
1062 # find machine id in model
1063 machine = None
1064 if machine_id is not None:
beierlmf52cb7c2020-04-21 16:36:35 -04001065 self.log.debug("Finding existing machine id {} in model".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001066 # get juju existing machines in the model
1067 existing_machines = await model.get_machines()
1068 if machine_id in existing_machines:
beierlmf52cb7c2020-04-21 16:36:35 -04001069 self.log.debug(
1070 "Machine id {} found in model (reusing it)".format(machine_id)
1071 )
quilesj29114342019-10-29 09:30:44 +01001072 machine = model.machines[machine_id]
1073
1074 if machine is None:
beierlmf52cb7c2020-04-21 16:36:35 -04001075 self.log.debug("Creating a new machine in juju...")
quilesj29114342019-10-29 09:30:44 +01001076 # machine does not exist, create it and wait for it
1077 machine = await model.add_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001078 spec=None, constraints=None, disks=None, series="xenial"
quilesj29114342019-10-29 09:30:44 +01001079 )
1080
1081 # register machine with observer
1082 observer.register_machine(machine=machine, db_dict=db_dict)
1083
1084 # id for the execution environment
1085 ee_id = N2VCJujuConnector._build_ee_id(
1086 model_name=model_name,
1087 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001088 machine_id=str(machine.entity_id),
quilesj29114342019-10-29 09:30:44 +01001089 )
1090
1091 # write ee_id in database
beierlmf52cb7c2020-04-21 16:36:35 -04001092 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
quilesj29114342019-10-29 09:30:44 +01001093
1094 # wait for machine creation
1095 await observer.wait_for_machine(
1096 machine_id=str(machine.entity_id),
1097 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001098 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001099 )
1100
1101 else:
1102
beierlmf52cb7c2020-04-21 16:36:35 -04001103 self.log.debug("Reusing old machine pending")
quilesj29114342019-10-29 09:30:44 +01001104
1105 # register machine with observer
1106 observer.register_machine(machine=machine, db_dict=db_dict)
1107
beierlmf52cb7c2020-04-21 16:36:35 -04001108 # machine does exist, but it is in creation process (pending), wait for
1109 # create finalisation
quilesj29114342019-10-29 09:30:44 +01001110 await observer.wait_for_machine(
1111 machine_id=machine.entity_id,
1112 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001113 total_timeout=total_timeout,
1114 )
quilesj29114342019-10-29 09:30:44 +01001115
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001116 self.log.debug("Machine ready at " + str(machine.dns_name))
quilesj29114342019-10-29 09:30:44 +01001117 return machine
1118
1119 async def _juju_provision_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001120 self,
1121 model_name: str,
1122 hostname: str,
1123 username: str,
1124 private_key_path: str,
1125 db_dict: dict = None,
1126 progress_timeout: float = None,
1127 total_timeout: float = None,
quilesjac4e0de2019-11-27 16:12:02 +00001128 ) -> str:
quilesj29114342019-10-29 09:30:44 +01001129
quilesjac4e0de2019-11-27 16:12:02 +00001130 if not self.api_proxy:
beierlmf52cb7c2020-04-21 16:36:35 -04001131 msg = "Cannot provision machine: api_proxy is not defined"
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001132 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001133 raise N2VCException(message=msg)
1134
beierlmf52cb7c2020-04-21 16:36:35 -04001135 self.log.debug(
1136 "provisioning machine. model: {}, hostname: {}, username: {}".format(
1137 model_name, hostname, username
1138 )
1139 )
quilesj29114342019-10-29 09:30:44 +01001140
1141 if not self._authenticated:
1142 await self._juju_login()
1143
1144 # get juju model and observer
1145 model = await self._juju_get_model(model_name=model_name)
1146 observer = self.juju_observers[model_name]
1147
quilesjac4e0de2019-11-27 16:12:02 +00001148 # TODO check if machine is already provisioned
1149 machine_list = await model.get_machines()
1150
David Garciae370f3b2020-04-06 12:42:26 +02001151 provisioner = AsyncSSHProvisioner(
quilesjac4e0de2019-11-27 16:12:02 +00001152 host=hostname,
1153 user=username,
1154 private_key_path=private_key_path,
beierlmf52cb7c2020-04-21 16:36:35 -04001155 log=self.log,
quilesjac4e0de2019-11-27 16:12:02 +00001156 )
1157
1158 params = None
quilesj29114342019-10-29 09:30:44 +01001159 try:
David Garciae370f3b2020-04-06 12:42:26 +02001160 params = await provisioner.provision_machine()
quilesjac4e0de2019-11-27 16:12:02 +00001161 except Exception as ex:
1162 msg = "Exception provisioning machine: {}".format(ex)
1163 self.log.error(msg)
1164 raise N2VCException(message=msg)
1165
beierlmf52cb7c2020-04-21 16:36:35 -04001166 params.jobs = ["JobHostUnits"]
quilesjac4e0de2019-11-27 16:12:02 +00001167
1168 connection = model.connection()
1169
1170 # Submit the request.
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001171 self.log.debug("Adding machine to model")
quilesjac4e0de2019-11-27 16:12:02 +00001172 client_facade = client.ClientFacade.from_connection(connection)
1173 results = await client_facade.AddMachines(params=[params])
1174 error = results.machines[0].error
1175 if error:
beierlm0a8c9af2020-05-12 15:26:37 -04001176 msg = "Error adding machine: {}".format(error.message)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001177 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001178 raise ValueError(msg)
1179
1180 machine_id = results.machines[0].machine
1181
1182 # Need to run this after AddMachines has been called,
1183 # as we need the machine_id
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001184 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
beierlmf52cb7c2020-04-21 16:36:35 -04001185 asyncio.ensure_future(
1186 provisioner.install_agent(
1187 connection=connection,
1188 nonce=params.nonce,
1189 machine_id=machine_id,
1190 api=self.api_proxy,
1191 )
1192 )
quilesjac4e0de2019-11-27 16:12:02 +00001193
beierlmf52cb7c2020-04-21 16:36:35 -04001194 # wait for machine in model (now, machine is not yet in model, so we must
1195 # wait for it)
quilesjac4e0de2019-11-27 16:12:02 +00001196 machine = None
beierlmf52cb7c2020-04-21 16:36:35 -04001197 for _ in range(10):
quilesjac4e0de2019-11-27 16:12:02 +00001198 machine_list = await model.get_machines()
1199 if machine_id in machine_list:
beierlmf52cb7c2020-04-21 16:36:35 -04001200 self.log.debug("Machine {} found in model!".format(machine_id))
quilesjac4e0de2019-11-27 16:12:02 +00001201 machine = model.machines.get(machine_id)
1202 break
1203 await asyncio.sleep(2)
1204
1205 if machine is None:
beierlmf52cb7c2020-04-21 16:36:35 -04001206 msg = "Machine {} not found in model".format(machine_id)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001207 self.log.error(msg=msg)
quilesjac4e0de2019-11-27 16:12:02 +00001208 raise Exception(msg)
quilesj29114342019-10-29 09:30:44 +01001209
1210 # register machine with observer
1211 observer.register_machine(machine=machine, db_dict=db_dict)
1212
1213 # wait for machine creation
beierlmf52cb7c2020-04-21 16:36:35 -04001214 self.log.debug("waiting for provision finishes... {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001215 await observer.wait_for_machine(
quilesjac4e0de2019-11-27 16:12:02 +00001216 machine_id=machine_id,
quilesj29114342019-10-29 09:30:44 +01001217 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001218 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001219 )
1220
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001221 self.log.debug("Machine provisioned {}".format(machine_id))
quilesjac4e0de2019-11-27 16:12:02 +00001222
1223 return machine_id
quilesj29114342019-10-29 09:30:44 +01001224
1225 async def _juju_deploy_charm(
beierlmf52cb7c2020-04-21 16:36:35 -04001226 self,
1227 model_name: str,
1228 application_name: str,
1229 charm_path: str,
1230 machine_id: str,
1231 db_dict: dict,
1232 progress_timeout: float = None,
1233 total_timeout: float = None,
1234 config: dict = None,
quilesj29114342019-10-29 09:30:44 +01001235 ) -> (Application, int):
1236
1237 # get juju model and observer
1238 model = await self._juju_get_model(model_name=model_name)
1239 observer = self.juju_observers[model_name]
1240
1241 # check if application already exists
1242 application = None
1243 if application_name in model.applications:
1244 application = model.applications[application_name]
1245
1246 if application is None:
1247
1248 # application does not exist, create it and wait for it
beierlmf52cb7c2020-04-21 16:36:35 -04001249 self.log.debug(
1250 "deploying application {} to machine {}, model {}".format(
1251 application_name, machine_id, model_name
1252 )
1253 )
1254 self.log.debug("charm: {}".format(charm_path))
David Garciaaf6812a2020-05-25 16:23:20 +02001255 machine = model.machines[machine_id]
quilesjac4e0de2019-11-27 16:12:02 +00001256 # series = None
quilesj29114342019-10-29 09:30:44 +01001257 application = await model.deploy(
1258 entity_url=charm_path,
1259 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001260 channel="stable",
quilesj29114342019-10-29 09:30:44 +01001261 num_units=1,
David Garciaaf6812a2020-05-25 16:23:20 +02001262 series=machine.series,
David Garciadfaa6e82020-04-01 16:06:39 +02001263 to=machine_id,
beierlmf52cb7c2020-04-21 16:36:35 -04001264 config=config,
quilesj29114342019-10-29 09:30:44 +01001265 )
1266
1267 # register application with observer
1268 observer.register_application(application=application, db_dict=db_dict)
1269
beierlmf52cb7c2020-04-21 16:36:35 -04001270 self.log.debug(
1271 "waiting for application deployed... {}".format(application.entity_id)
1272 )
quilesj29114342019-10-29 09:30:44 +01001273 retries = await observer.wait_for_application(
1274 application_id=application.entity_id,
1275 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001276 total_timeout=total_timeout,
1277 )
1278 self.log.debug("application deployed")
quilesj29114342019-10-29 09:30:44 +01001279
1280 else:
1281
1282 # register application with observer
1283 observer.register_application(application=application, db_dict=db_dict)
1284
1285 # application already exists, but not finalised
beierlmf52cb7c2020-04-21 16:36:35 -04001286 self.log.debug("application already exists, waiting for deployed...")
quilesj29114342019-10-29 09:30:44 +01001287 retries = await observer.wait_for_application(
1288 application_id=application.entity_id,
1289 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001290 total_timeout=total_timeout,
1291 )
1292 self.log.debug("application deployed")
quilesj29114342019-10-29 09:30:44 +01001293
1294 return application, retries
1295
1296 async def _juju_execute_action(
beierlmf52cb7c2020-04-21 16:36:35 -04001297 self,
1298 model_name: str,
1299 application_name: str,
1300 action_name: str,
1301 db_dict: dict,
1302 progress_timeout: float = None,
1303 total_timeout: float = None,
1304 **kwargs
quilesj29114342019-10-29 09:30:44 +01001305 ) -> Action:
1306
1307 # get juju model and observer
1308 model = await self._juju_get_model(model_name=model_name)
1309 observer = self.juju_observers[model_name]
1310
beierlmf52cb7c2020-04-21 16:36:35 -04001311 application = await self._juju_get_application(
1312 model_name=model_name, application_name=application_name
1313 )
quilesj29114342019-10-29 09:30:44 +01001314
David Garciaeee9ead2020-03-25 16:23:14 +01001315 unit = None
1316 for u in application.units:
1317 if await u.is_leader_from_status():
1318 unit = u
quilesj29114342019-10-29 09:30:44 +01001319 if unit is not None:
1320 actions = await application.get_actions()
1321 if action_name in actions:
beierlmf52cb7c2020-04-21 16:36:35 -04001322 self.log.debug(
1323 'executing action "{}" using params: {}'.format(action_name, kwargs)
1324 )
quilesj29114342019-10-29 09:30:44 +01001325 action = await unit.run_action(action_name, **kwargs)
1326
1327 # register action with observer
1328 observer.register_action(action=action, db_dict=db_dict)
1329
quilesj29114342019-10-29 09:30:44 +01001330 await observer.wait_for_action(
1331 action_id=action.entity_id,
1332 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001333 total_timeout=total_timeout,
1334 )
1335 self.log.debug("action completed with status: {}".format(action.status))
quilesj29114342019-10-29 09:30:44 +01001336 output = await model.get_action_output(action_uuid=action.entity_id)
1337 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1338 if action.entity_id in status:
1339 status = status[action.entity_id]
1340 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001341 status = "failed"
quilesj29114342019-10-29 09:30:44 +01001342 return output, status
1343
1344 raise N2VCExecutionException(
beierlmf52cb7c2020-04-21 16:36:35 -04001345 message="Cannot execute action on charm", primitive_name=action_name
quilesj29114342019-10-29 09:30:44 +01001346 )
1347
1348 async def _juju_configure_application(
beierlmf52cb7c2020-04-21 16:36:35 -04001349 self,
1350 model_name: str,
1351 application_name: str,
1352 config: dict,
1353 db_dict: dict,
1354 progress_timeout: float = None,
1355 total_timeout: float = None,
quilesj29114342019-10-29 09:30:44 +01001356 ):
1357
quilesj29114342019-10-29 09:30:44 +01001358 # get the application
beierlmf52cb7c2020-04-21 16:36:35 -04001359 application = await self._juju_get_application(
1360 model_name=model_name, application_name=application_name
1361 )
quilesj29114342019-10-29 09:30:44 +01001362
beierlmf52cb7c2020-04-21 16:36:35 -04001363 self.log.debug(
1364 "configuring the application {} -> {}".format(application_name, config)
1365 )
quilesj29114342019-10-29 09:30:44 +01001366 res = await application.set_config(config)
beierlmf52cb7c2020-04-21 16:36:35 -04001367 self.log.debug(
1368 "application {} configured. res={}".format(application_name, res)
1369 )
quilesj29114342019-10-29 09:30:44 +01001370
1371 # Verify the config is set
1372 new_conf = await application.get_config()
1373 for key in config:
beierlmf52cb7c2020-04-21 16:36:35 -04001374 value = new_conf[key]["value"]
1375 self.log.debug(" {} = {}".format(key, value))
quilesj29114342019-10-29 09:30:44 +01001376 if config[key] != value:
1377 raise N2VCException(
beierlmf52cb7c2020-04-21 16:36:35 -04001378 message="key {} is not configured correctly {} != {}".format(
1379 key, config[key], new_conf[key]
1380 )
quilesj29114342019-10-29 09:30:44 +01001381 )
1382
1383 # check if 'verify-ssh-credentials' action exists
quilesj073e1692019-11-29 11:19:14 +00001384 # unit = application.units[0]
quilesj29114342019-10-29 09:30:44 +01001385 actions = await application.get_actions()
beierlmf52cb7c2020-04-21 16:36:35 -04001386 if "verify-ssh-credentials" not in actions:
1387 msg = (
1388 "Action verify-ssh-credentials does not exist in application {}"
1389 ).format(application_name)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001390 self.log.debug(msg=msg)
quilesj29114342019-10-29 09:30:44 +01001391 return False
1392
1393 # execute verify-credentials
1394 num_retries = 20
1395 retry_timeout = 15.0
beierlmf52cb7c2020-04-21 16:36:35 -04001396 for _ in range(num_retries):
quilesj29114342019-10-29 09:30:44 +01001397 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001398 self.log.debug("Executing action verify-ssh-credentials...")
quilesj29114342019-10-29 09:30:44 +01001399 output, ok = await self._juju_execute_action(
1400 model_name=model_name,
1401 application_name=application_name,
beierlmf52cb7c2020-04-21 16:36:35 -04001402 action_name="verify-ssh-credentials",
quilesj29114342019-10-29 09:30:44 +01001403 db_dict=db_dict,
1404 progress_timeout=progress_timeout,
beierlmf52cb7c2020-04-21 16:36:35 -04001405 total_timeout=total_timeout,
quilesj29114342019-10-29 09:30:44 +01001406 )
beierlmf52cb7c2020-04-21 16:36:35 -04001407 self.log.debug("Result: {}, output: {}".format(ok, output))
quilesj29114342019-10-29 09:30:44 +01001408 return True
tierno8ff11992020-03-26 09:51:11 +00001409 except asyncio.CancelledError:
1410 raise
quilesj29114342019-10-29 09:30:44 +01001411 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001412 self.log.debug(
1413 "Error executing verify-ssh-credentials: {}. Retrying...".format(e)
1414 )
quilesj29114342019-10-29 09:30:44 +01001415 await asyncio.sleep(retry_timeout)
1416 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001417 self.log.error(
1418 "Error executing verify-ssh-credentials after {} retries. ".format(
1419 num_retries
1420 )
1421 )
quilesj29114342019-10-29 09:30:44 +01001422 return False
1423
beierlmf52cb7c2020-04-21 16:36:35 -04001424 async def _juju_get_application(self, model_name: str, application_name: str):
quilesj29114342019-10-29 09:30:44 +01001425 """Get the deployed application."""
1426
1427 model = await self._juju_get_model(model_name=model_name)
1428
1429 application_name = N2VCJujuConnector._format_app_name(application_name)
1430
1431 if model.applications and application_name in model.applications:
1432 return model.applications[application_name]
1433 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001434 raise N2VCException(
1435 message="Cannot get application {} from model {}".format(
1436 application_name, model_name
1437 )
1438 )
quilesj29114342019-10-29 09:30:44 +01001439
1440 async def _juju_get_model(self, model_name: str) -> Model:
1441 """ Get a model object from juju controller
garciadeblas923510c2019-12-17 15:02:11 +01001442 If the model does not exits, it creates it.
quilesj29114342019-10-29 09:30:44 +01001443
1444 :param str model_name: name of the model
1445 :returns Model: model obtained from juju controller or Exception
1446 """
1447
1448 # format model name
1449 model_name = N2VCJujuConnector._format_model_name(model_name)
1450
1451 if model_name in self.juju_models:
1452 return self.juju_models[model_name]
1453
1454 if self._creating_model:
beierlmf52cb7c2020-04-21 16:36:35 -04001455 self.log.debug("Another coroutine is creating a model. Wait...")
quilesj29114342019-10-29 09:30:44 +01001456 while self._creating_model:
1457 # another coroutine is creating a model, wait
1458 await asyncio.sleep(0.1)
1459 # retry (perhaps another coroutine has created the model meanwhile)
1460 if model_name in self.juju_models:
1461 return self.juju_models[model_name]
1462
1463 try:
1464 self._creating_model = True
1465
1466 # get juju model names from juju
1467 model_list = await self.controller.list_models()
quilesj29114342019-10-29 09:30:44 +01001468 if model_name not in model_list:
beierlmf52cb7c2020-04-21 16:36:35 -04001469 self.log.info(
1470 "Model {} does not exist. Creating new model...".format(model_name)
1471 )
1472 config_dict = {"authorized-keys": self.public_key}
garciadeblas923510c2019-12-17 15:02:11 +01001473 if self.apt_mirror:
beierlmf52cb7c2020-04-21 16:36:35 -04001474 config_dict["apt-mirror"] = self.apt_mirror
garciadeblas923510c2019-12-17 15:02:11 +01001475 if not self.enable_os_upgrade:
David Garciae370f3b2020-04-06 12:42:26 +02001476 config_dict["enable-os-refresh-update"] = False
1477 config_dict["enable-os-upgrade"] = False
David Garcia347aae62020-04-29 12:34:23 +02001478 if self.cloud in self.BUILT_IN_CLOUDS:
1479 model = await self.controller.add_model(
1480 model_name=model_name,
1481 config=config_dict,
1482 cloud_name=self.cloud,
1483 )
1484 else:
1485 model = await self.controller.add_model(
1486 model_name=model_name,
1487 config=config_dict,
1488 cloud_name=self.cloud,
David Garcia32b38122020-05-07 12:28:05 +02001489 credential_name=self.cloud,
David Garcia347aae62020-04-29 12:34:23 +02001490 )
David Garciae370f3b2020-04-06 12:42:26 +02001491 self.log.info("New model created, name={}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001492 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001493 self.log.debug(
1494 "Model already exists in juju. Getting model {}".format(model_name)
1495 )
quilesj29114342019-10-29 09:30:44 +01001496 model = await self.controller.get_model(model_name)
beierlmf52cb7c2020-04-21 16:36:35 -04001497 self.log.debug("Existing model in juju, name={}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001498
1499 self.juju_models[model_name] = model
1500 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1501 return model
1502
1503 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001504 msg = "Cannot get model {}. Exception: {}".format(model_name, e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001505 self.log.error(msg)
quilesj29114342019-10-29 09:30:44 +01001506 raise N2VCException(msg)
1507 finally:
1508 self._creating_model = False
1509
1510 async def _juju_add_relation(
beierlmf52cb7c2020-04-21 16:36:35 -04001511 self,
1512 model_name: str,
1513 application_name_1: str,
1514 application_name_2: str,
1515 relation_1: str,
1516 relation_2: str,
quilesj29114342019-10-29 09:30:44 +01001517 ):
1518
quilesj29114342019-10-29 09:30:44 +01001519 # get juju model and observer
1520 model = await self._juju_get_model(model_name=model_name)
1521
beierlmf52cb7c2020-04-21 16:36:35 -04001522 r1 = "{}:{}".format(application_name_1, relation_1)
1523 r2 = "{}:{}".format(application_name_2, relation_2)
quilesjaae10b42020-01-09 08:49:10 +00001524
beierlmf52cb7c2020-04-21 16:36:35 -04001525 self.log.debug("adding relation: {} -> {}".format(r1, r2))
quilesjaae10b42020-01-09 08:49:10 +00001526 try:
1527 await model.add_relation(relation1=r1, relation2=r2)
1528 except JujuAPIError as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001529 # If one of the applications in the relationship doesn't exist, or the
1530 # relation has already been added,
quilesjaae10b42020-01-09 08:49:10 +00001531 # let the operation fail silently.
beierlmf52cb7c2020-04-21 16:36:35 -04001532 if "not found" in e.message:
quilesjaae10b42020-01-09 08:49:10 +00001533 return
beierlmf52cb7c2020-04-21 16:36:35 -04001534 if "already exists" in e.message:
quilesjaae10b42020-01-09 08:49:10 +00001535 return
1536 # another execption, raise it
1537 raise e
quilesj29114342019-10-29 09:30:44 +01001538
beierlmf52cb7c2020-04-21 16:36:35 -04001539 async def _juju_destroy_application(self, model_name: str, application_name: str):
quilesj29114342019-10-29 09:30:44 +01001540
beierlmf52cb7c2020-04-21 16:36:35 -04001541 self.log.debug(
1542 "Destroying application {} in model {}".format(application_name, model_name)
1543 )
quilesj29114342019-10-29 09:30:44 +01001544
1545 # get juju model and observer
1546 model = await self._juju_get_model(model_name=model_name)
calvinosanch41395222020-02-21 09:25:21 +01001547 observer = self.juju_observers[model_name]
quilesj29114342019-10-29 09:30:44 +01001548
1549 application = model.applications.get(application_name)
1550 if application:
calvinosanch41395222020-02-21 09:25:21 +01001551 observer.unregister_application(application_name)
quilesj29114342019-10-29 09:30:44 +01001552 await application.destroy()
1553 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001554 self.log.debug("Application not found: {}".format(application_name))
quilesj29114342019-10-29 09:30:44 +01001555
1556 async def _juju_destroy_machine(
beierlmf52cb7c2020-04-21 16:36:35 -04001557 self, model_name: str, machine_id: str, total_timeout: float = None
quilesj29114342019-10-29 09:30:44 +01001558 ):
1559
beierlmf52cb7c2020-04-21 16:36:35 -04001560 self.log.debug(
1561 "Destroying machine {} in model {}".format(machine_id, model_name)
1562 )
quilesj29114342019-10-29 09:30:44 +01001563
1564 if total_timeout is None:
1565 total_timeout = 3600
1566
1567 # get juju model and observer
1568 model = await self._juju_get_model(model_name=model_name)
calvinosanch41395222020-02-21 09:25:21 +01001569 observer = self.juju_observers[model_name]
quilesj29114342019-10-29 09:30:44 +01001570
1571 machines = await model.get_machines()
1572 if machine_id in machines:
1573 machine = model.machines[machine_id]
calvinosanch41395222020-02-21 09:25:21 +01001574 observer.unregister_machine(machine_id)
beierlmf52cb7c2020-04-21 16:36:35 -04001575 # TODO: change this by machine.is_manual when this is upstreamed:
1576 # https://github.com/juju/python-libjuju/pull/396
David Garciadf9f72a2020-04-06 11:02:42 +02001577 if "instance-id" in machine.safe_data and machine.safe_data[
1578 "instance-id"
1579 ].startswith("manual:"):
1580 self.log.debug("machine.destroy(force=True) started.")
1581 await machine.destroy(force=True)
1582 self.log.debug("machine.destroy(force=True) passed.")
1583 # max timeout
1584 end = time.time() + total_timeout
1585 # wait for machine removal
quilesj29114342019-10-29 09:30:44 +01001586 machines = await model.get_machines()
David Garciadf9f72a2020-04-06 11:02:42 +02001587 while machine_id in machines and time.time() < end:
beierlmf52cb7c2020-04-21 16:36:35 -04001588 self.log.debug(
1589 "Waiting for machine {} is destroyed".format(machine_id)
1590 )
David Garciadf9f72a2020-04-06 11:02:42 +02001591 await asyncio.sleep(0.5)
1592 machines = await model.get_machines()
1593 self.log.debug("Machine destroyed: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001594 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001595 self.log.debug("Machine not found: {}".format(machine_id))
quilesj29114342019-10-29 09:30:44 +01001596
beierlmf52cb7c2020-04-21 16:36:35 -04001597 async def _juju_destroy_model(self, model_name: str, total_timeout: float = None):
quilesj29114342019-10-29 09:30:44 +01001598
beierlmf52cb7c2020-04-21 16:36:35 -04001599 self.log.debug("Destroying model {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001600
1601 if total_timeout is None:
1602 total_timeout = 3600
tierno8ff11992020-03-26 09:51:11 +00001603 end = time.time() + total_timeout
quilesj29114342019-10-29 09:30:44 +01001604
1605 model = await self._juju_get_model(model_name=model_name)
David Garcia979df142020-04-06 12:55:23 +02001606
1607 if not model:
beierlmf52cb7c2020-04-21 16:36:35 -04001608 raise N2VCNotFound(message="Model {} does not exist".format(model_name))
David Garcia979df142020-04-06 12:55:23 +02001609
quilesj29114342019-10-29 09:30:44 +01001610 uuid = model.info.uuid
1611
David Garciadf9f72a2020-04-06 11:02:42 +02001612 # destroy applications
1613 for application_name in model.applications:
1614 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001615 await self._juju_destroy_application(
1616 model_name=model_name, application_name=application_name
1617 )
David Garciadf9f72a2020-04-06 11:02:42 +02001618 except Exception as e:
1619 self.log.error(
1620 "Error destroying application {} in model {}: {}".format(
beierlmf52cb7c2020-04-21 16:36:35 -04001621 application_name, model_name, e
David Garciadf9f72a2020-04-06 11:02:42 +02001622 )
1623 )
1624
quilesjbd5a0c92020-01-15 12:30:09 +00001625 # destroy machines
1626 machines = await model.get_machines()
1627 for machine_id in machines:
1628 try:
beierlmf52cb7c2020-04-21 16:36:35 -04001629 await self._juju_destroy_machine(
1630 model_name=model_name, machine_id=machine_id
1631 )
tierno8ff11992020-03-26 09:51:11 +00001632 except asyncio.CancelledError:
1633 raise
beierlmf52cb7c2020-04-21 16:36:35 -04001634 except Exception:
quilesjbd5a0c92020-01-15 12:30:09 +00001635 # ignore exceptions destroying machine
1636 pass
1637
quilesj29114342019-10-29 09:30:44 +01001638 await self._juju_disconnect_model(model_name=model_name)
quilesj29114342019-10-29 09:30:44 +01001639
beierlmf52cb7c2020-04-21 16:36:35 -04001640 self.log.debug("destroying model {}...".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001641 await self.controller.destroy_model(uuid)
tierno8ff11992020-03-26 09:51:11 +00001642 # self.log.debug('model destroy requested {}'.format(model_name))
quilesj29114342019-10-29 09:30:44 +01001643
1644 # wait for model is completely destroyed
beierlmf52cb7c2020-04-21 16:36:35 -04001645 self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
1646 last_exception = ""
quilesj29114342019-10-29 09:30:44 +01001647 while time.time() < end:
quilesj29114342019-10-29 09:30:44 +01001648 try:
quilesj776ab392019-12-12 16:10:54 +00001649 # await self.controller.get_model(uuid)
1650 models = await self.controller.list_models()
1651 if model_name not in models:
beierlmf52cb7c2020-04-21 16:36:35 -04001652 self.log.debug(
1653 "The model {} ({}) was destroyed".format(model_name, uuid)
1654 )
quilesj776ab392019-12-12 16:10:54 +00001655 return
tierno8ff11992020-03-26 09:51:11 +00001656 except asyncio.CancelledError:
1657 raise
quilesj776ab392019-12-12 16:10:54 +00001658 except Exception as e:
tierno8ff11992020-03-26 09:51:11 +00001659 last_exception = e
1660 await asyncio.sleep(5)
beierlmf52cb7c2020-04-21 16:36:35 -04001661 raise N2VCException(
1662 "Timeout waiting for model {} to be destroyed {}".format(
1663 model_name, last_exception
1664 )
1665 )
quilesj29114342019-10-29 09:30:44 +01001666
1667 async def _juju_login(self):
1668 """Connect to juju controller
1669
1670 """
1671
1672 # if already authenticated, exit function
1673 if self._authenticated:
1674 return
1675
1676 # if connecting, wait for finish
1677 # another task could be trying to connect in parallel
1678 while self._connecting:
1679 await asyncio.sleep(0.1)
1680
1681 # double check after other task has finished
1682 if self._authenticated:
1683 return
1684
1685 try:
1686 self._connecting = True
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001687 self.log.info(
beierlmf52cb7c2020-04-21 16:36:35 -04001688 "connecting to juju controller: {} {}:{}{}".format(
1689 self.url,
1690 self.username,
1691 self.secret[:8] + "...",
1692 " with ca_cert" if self.ca_cert else "",
1693 )
1694 )
quilesj29114342019-10-29 09:30:44 +01001695
1696 # Create controller object
1697 self.controller = Controller(loop=self.loop)
1698 # Connect to controller
1699 await self.controller.connect(
1700 endpoint=self.url,
1701 username=self.username,
1702 password=self.secret,
beierlmf52cb7c2020-04-21 16:36:35 -04001703 cacert=self.ca_cert,
quilesj29114342019-10-29 09:30:44 +01001704 )
1705 self._authenticated = True
beierlmf52cb7c2020-04-21 16:36:35 -04001706 self.log.info("juju controller connected")
quilesj29114342019-10-29 09:30:44 +01001707 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001708 message = "Exception connecting to juju: {}".format(e)
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001709 self.log.error(message)
beierlmf52cb7c2020-04-21 16:36:35 -04001710 raise N2VCConnectionException(message=message, url=self.url)
quilesj29114342019-10-29 09:30:44 +01001711 finally:
1712 self._connecting = False
1713
1714 async def _juju_logout(self):
1715 """Logout of the Juju controller."""
1716 if not self._authenticated:
1717 return False
1718
1719 # disconnect all models
1720 for model_name in self.juju_models:
1721 try:
1722 await self._juju_disconnect_model(model_name)
1723 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001724 self.log.error(
1725 "Error disconnecting model {} : {}".format(model_name, e)
1726 )
quilesj29114342019-10-29 09:30:44 +01001727 # continue with next model...
1728
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001729 self.log.info("Disconnecting controller")
quilesj29114342019-10-29 09:30:44 +01001730 try:
1731 await self.controller.disconnect()
1732 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -04001733 raise N2VCConnectionException(
1734 message="Error disconnecting controller: {}".format(e), url=self.url
1735 )
quilesj29114342019-10-29 09:30:44 +01001736
1737 self.controller = None
1738 self._authenticated = False
beierlmf52cb7c2020-04-21 16:36:35 -04001739 self.log.info("disconnected")
quilesj29114342019-10-29 09:30:44 +01001740
beierlmf52cb7c2020-04-21 16:36:35 -04001741 async def _juju_disconnect_model(self, model_name: str):
Dominik Fleischmannf9bed352020-02-27 10:04:34 +01001742 self.log.debug("Disconnecting model {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001743 if model_name in self.juju_models:
1744 await self.juju_models[model_name].disconnect()
1745 self.juju_models[model_name] = None
1746 self.juju_observers[model_name] = None
quilesj776ab392019-12-12 16:10:54 +00001747 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001748 self.warning("Cannot disconnect model: {}".format(model_name))
quilesj29114342019-10-29 09:30:44 +01001749
1750 def _create_juju_public_key(self):
1751 """Recreate the Juju public key on lcm container, if needed
1752 Certain libjuju commands expect to be run from the same machine as Juju
1753 is bootstrapped to. This method will write the public key to disk in
1754 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1755 """
1756
1757 # Make sure that we have a public key before writing to disk
1758 if self.public_key is None or len(self.public_key) == 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001759 if "OSMLCM_VCA_PUBKEY" in os.environ:
1760 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
quilesj29114342019-10-29 09:30:44 +01001761 if len(self.public_key) == 0:
1762 return
1763 else:
1764 return
1765
beierlmf52cb7c2020-04-21 16:36:35 -04001766 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
quilesj29114342019-10-29 09:30:44 +01001767 file_path = "{}/juju_id_rsa.pub".format(pk_path)
beierlmf52cb7c2020-04-21 16:36:35 -04001768 self.log.debug(
1769 "writing juju public key to file:\n{}\npublic key: {}".format(
1770 file_path, self.public_key
1771 )
1772 )
quilesj29114342019-10-29 09:30:44 +01001773 if not os.path.exists(pk_path):
1774 # create path and write file
1775 os.makedirs(pk_path)
beierlmf52cb7c2020-04-21 16:36:35 -04001776 with open(file_path, "w") as f:
1777 self.log.debug("Creating juju public key file: {}".format(file_path))
quilesj29114342019-10-29 09:30:44 +01001778 f.write(self.public_key)
1779 else:
beierlmf52cb7c2020-04-21 16:36:35 -04001780 self.log.debug("juju public key file already exists: {}".format(file_path))
quilesj29114342019-10-29 09:30:44 +01001781
1782 @staticmethod
1783 def _format_model_name(name: str) -> str:
1784 """Format the name of the model.
1785
1786 Model names may only contain lowercase letters, digits and hyphens
1787 """
1788
beierlmf52cb7c2020-04-21 16:36:35 -04001789 return name.replace("_", "-").replace(" ", "-").lower()
quilesj29114342019-10-29 09:30:44 +01001790
1791 @staticmethod
1792 def _format_app_name(name: str) -> str:
1793 """Format the name of the application (in order to assure valid application name).
1794
1795 Application names have restrictions (run juju deploy --help):
1796 - contains lowercase letters 'a'-'z'
1797 - contains numbers '0'-'9'
1798 - contains hyphens '-'
1799 - starts with a lowercase letter
1800 - not two or more consecutive hyphens
1801 - after a hyphen, not a group with all numbers
1802 """
1803
1804 def all_numbers(s: str) -> bool:
1805 for c in s:
1806 if not c.isdigit():
1807 return False
1808 return True
1809
beierlmf52cb7c2020-04-21 16:36:35 -04001810 new_name = name.replace("_", "-")
1811 new_name = new_name.replace(" ", "-")
quilesj29114342019-10-29 09:30:44 +01001812 new_name = new_name.lower()
beierlmf52cb7c2020-04-21 16:36:35 -04001813 while new_name.find("--") >= 0:
1814 new_name = new_name.replace("--", "-")
1815 groups = new_name.split("-")
quilesj29114342019-10-29 09:30:44 +01001816
1817 # find 'all numbers' groups and prefix them with a letter
beierlmf52cb7c2020-04-21 16:36:35 -04001818 app_name = ""
quilesj29114342019-10-29 09:30:44 +01001819 for i in range(len(groups)):
1820 group = groups[i]
1821 if all_numbers(group):
beierlmf52cb7c2020-04-21 16:36:35 -04001822 group = "z" + group
quilesj29114342019-10-29 09:30:44 +01001823 if i > 0:
beierlmf52cb7c2020-04-21 16:36:35 -04001824 app_name += "-"
quilesj29114342019-10-29 09:30:44 +01001825 app_name += group
1826
1827 if app_name[0].isdigit():
beierlmf52cb7c2020-04-21 16:36:35 -04001828 app_name = "z" + app_name
quilesj29114342019-10-29 09:30:44 +01001829
1830 return app_name