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