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 |
1 |
import asyncio |
24 |
1 |
import logging |
25 |
1 |
import os |
26 |
|
|
27 |
1 |
from n2vc.config import ModelConfig |
28 |
1 |
from n2vc.exceptions import ( |
29 |
|
N2VCBadArgumentsException, |
30 |
|
N2VCException, |
31 |
|
N2VCConnectionException, |
32 |
|
N2VCExecutionException, |
33 |
|
# N2VCNotFound, |
34 |
|
MethodNotImplemented, |
35 |
|
JujuK8sProxycharmNotSupported, |
36 |
|
) |
37 |
1 |
from n2vc.n2vc_conn import N2VCConnector |
38 |
1 |
from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml |
39 |
1 |
from n2vc.libjuju import Libjuju |
40 |
1 |
from n2vc.utils import base64_to_cacert |
41 |
|
|
42 |
|
|
43 |
1 |
class N2VCJujuConnector(N2VCConnector): |
44 |
|
|
45 |
|
""" |
46 |
|
#################################################################################### |
47 |
|
################################### P U B L I C #################################### |
48 |
|
#################################################################################### |
49 |
|
""" |
50 |
|
|
51 |
1 |
BUILT_IN_CLOUDS = ["localhost", "microk8s"] |
52 |
|
|
53 |
1 |
def __init__( |
54 |
|
self, |
55 |
|
db: object, |
56 |
|
fs: object, |
57 |
|
log: object = None, |
58 |
|
loop: object = None, |
59 |
|
url: str = "127.0.0.1:17070", |
60 |
|
username: str = "admin", |
61 |
|
vca_config: dict = None, |
62 |
|
on_update_db=None, |
63 |
|
): |
64 |
|
"""Initialize juju N2VC connector |
65 |
|
""" |
66 |
|
|
67 |
|
# parent class constructor |
68 |
1 |
N2VCConnector.__init__( |
69 |
|
self, |
70 |
|
db=db, |
71 |
|
fs=fs, |
72 |
|
log=log, |
73 |
|
loop=loop, |
74 |
|
url=url, |
75 |
|
username=username, |
76 |
|
vca_config=vca_config, |
77 |
|
on_update_db=on_update_db, |
78 |
|
) |
79 |
|
|
80 |
|
# silence websocket traffic log |
81 |
1 |
logging.getLogger("websockets.protocol").setLevel(logging.INFO) |
82 |
1 |
logging.getLogger("juju.client.connection").setLevel(logging.WARN) |
83 |
1 |
logging.getLogger("model").setLevel(logging.WARN) |
84 |
|
|
85 |
1 |
self.log.info("Initializing N2VC juju connector...") |
86 |
|
|
87 |
|
""" |
88 |
|
############################################################## |
89 |
|
# check arguments |
90 |
|
############################################################## |
91 |
|
""" |
92 |
|
|
93 |
|
# juju URL |
94 |
1 |
if url is None: |
95 |
0 |
raise N2VCBadArgumentsException("Argument url is mandatory", ["url"]) |
96 |
1 |
url_parts = url.split(":") |
97 |
1 |
if len(url_parts) != 2: |
98 |
0 |
raise N2VCBadArgumentsException( |
99 |
|
"Argument url: bad format (localhost:port) -> {}".format(url), ["url"] |
100 |
|
) |
101 |
1 |
self.hostname = url_parts[0] |
102 |
1 |
try: |
103 |
1 |
self.port = int(url_parts[1]) |
104 |
0 |
except ValueError: |
105 |
0 |
raise N2VCBadArgumentsException( |
106 |
|
"url port must be a number -> {}".format(url), ["url"] |
107 |
|
) |
108 |
|
|
109 |
|
# juju USERNAME |
110 |
1 |
if username is None: |
111 |
0 |
raise N2VCBadArgumentsException( |
112 |
|
"Argument username is mandatory", ["username"] |
113 |
|
) |
114 |
|
|
115 |
|
# juju CONFIGURATION |
116 |
1 |
if vca_config is None: |
117 |
0 |
raise N2VCBadArgumentsException( |
118 |
|
"Argument vca_config is mandatory", ["vca_config"] |
119 |
|
) |
120 |
|
|
121 |
1 |
if "secret" in vca_config: |
122 |
1 |
self.secret = vca_config["secret"] |
123 |
|
else: |
124 |
0 |
raise N2VCBadArgumentsException( |
125 |
|
"Argument vca_config.secret is mandatory", ["vca_config.secret"] |
126 |
|
) |
127 |
|
|
128 |
|
# pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub |
129 |
|
# if exists, it will be written in lcm container: _create_juju_public_key() |
130 |
1 |
if "public_key" in vca_config: |
131 |
0 |
self.public_key = vca_config["public_key"] |
132 |
|
else: |
133 |
1 |
self.public_key = None |
134 |
|
|
135 |
|
# TODO: Verify ca_cert is valid before using. VCA will crash |
136 |
|
# if the ca_cert isn't formatted correctly. |
137 |
|
|
138 |
1 |
self.ca_cert = vca_config.get("ca_cert") |
139 |
1 |
if self.ca_cert: |
140 |
0 |
self.ca_cert = base64_to_cacert(vca_config["ca_cert"]) |
141 |
|
|
142 |
1 |
if "api_proxy" in vca_config and vca_config["api_proxy"] != "": |
143 |
1 |
self.api_proxy = vca_config["api_proxy"] |
144 |
1 |
self.log.debug( |
145 |
|
"api_proxy for native charms configured: {}".format(self.api_proxy) |
146 |
|
) |
147 |
|
else: |
148 |
0 |
self.warning( |
149 |
|
"api_proxy is not configured" |
150 |
|
) |
151 |
0 |
self.api_proxy = None |
152 |
|
|
153 |
1 |
model_config = ModelConfig(vca_config) |
154 |
|
|
155 |
1 |
self.cloud = vca_config.get('cloud') |
156 |
1 |
self.k8s_cloud = None |
157 |
1 |
if "k8s_cloud" in vca_config: |
158 |
1 |
self.k8s_cloud = vca_config.get("k8s_cloud") |
159 |
1 |
self.log.debug('Arguments have been checked') |
160 |
|
|
161 |
|
# juju data |
162 |
1 |
self.controller = None # it will be filled when connect to juju |
163 |
1 |
self.juju_models = {} # model objects for every model_name |
164 |
1 |
self.juju_observers = {} # model observers for every model_name |
165 |
1 |
self._connecting = ( |
166 |
|
False # while connecting to juju (to avoid duplicate connections) |
167 |
|
) |
168 |
1 |
self._authenticated = ( |
169 |
|
False # it will be True when juju connection be stablished |
170 |
|
) |
171 |
1 |
self._creating_model = False # True during model creation |
172 |
1 |
self.libjuju = Libjuju( |
173 |
|
endpoint=self.url, |
174 |
|
api_proxy=self.api_proxy, |
175 |
|
username=self.username, |
176 |
|
password=self.secret, |
177 |
|
cacert=self.ca_cert, |
178 |
|
loop=self.loop, |
179 |
|
log=self.log, |
180 |
|
db=self.db, |
181 |
|
n2vc=self, |
182 |
|
model_config=model_config, |
183 |
|
) |
184 |
|
|
185 |
|
# create juju pub key file in lcm container at |
186 |
|
# ./local/share/juju/ssh/juju_id_rsa.pub |
187 |
1 |
self._create_juju_public_key() |
188 |
|
|
189 |
1 |
self.log.info("N2VC juju connector initialized") |
190 |
|
|
191 |
1 |
async def get_status(self, namespace: str, yaml_format: bool = True): |
192 |
|
|
193 |
|
# self.log.info('Getting NS status. namespace: {}'.format(namespace)) |
194 |
|
|
195 |
0 |
_nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( |
196 |
|
namespace=namespace |
197 |
|
) |
198 |
|
# model name is ns_id |
199 |
0 |
model_name = ns_id |
200 |
0 |
if model_name is None: |
201 |
0 |
msg = "Namespace {} not valid".format(namespace) |
202 |
0 |
self.log.error(msg) |
203 |
0 |
raise N2VCBadArgumentsException(msg, ["namespace"]) |
204 |
|
|
205 |
0 |
status = {} |
206 |
0 |
models = await self.libjuju.list_models(contains=ns_id) |
207 |
|
|
208 |
0 |
for m in models: |
209 |
0 |
status[m] = await self.libjuju.get_model_status(m) |
210 |
0 |
if yaml_format: |
211 |
0 |
return obj_to_yaml(status) |
212 |
|
else: |
213 |
0 |
return obj_to_dict(status) |
214 |
|
|
215 |
1 |
async def update_vca_status(self, vcastatus: dict): |
216 |
|
""" |
217 |
|
Add all configs, actions, executed actions of all applications in a model to vcastatus dict. |
218 |
|
:param vcastatus: dict containing vcaStatus |
219 |
|
:return: None |
220 |
|
""" |
221 |
1 |
try: |
222 |
1 |
for model_name in vcastatus: |
223 |
|
# Adding executed actions |
224 |
1 |
vcastatus[model_name]["executedActions"] = \ |
225 |
|
await self.libjuju.get_executed_actions(model_name) |
226 |
1 |
for application in vcastatus[model_name]["applications"]: |
227 |
|
# Adding application actions |
228 |
1 |
vcastatus[model_name]["applications"][application]["actions"] = \ |
229 |
|
await self.libjuju.get_actions(application, model_name) |
230 |
|
# Adding application configs |
231 |
1 |
vcastatus[model_name]["applications"][application]["configs"] = \ |
232 |
|
await self.libjuju.get_application_configs(model_name, application) |
233 |
1 |
except Exception as e: |
234 |
1 |
self.log.debug("Error in updating vca status: {}".format(str(e))) |
235 |
|
|
236 |
1 |
async def create_execution_environment( |
237 |
|
self, |
238 |
|
namespace: str, |
239 |
|
db_dict: dict, |
240 |
|
reuse_ee_id: str = None, |
241 |
|
progress_timeout: float = None, |
242 |
|
total_timeout: float = None, |
243 |
|
cloud_name: str = None, |
244 |
|
credential_name: str = None, |
245 |
|
) -> (str, dict): |
246 |
|
|
247 |
0 |
self.log.info( |
248 |
|
"Creating execution environment. namespace: {}, reuse_ee_id: {}".format( |
249 |
|
namespace, reuse_ee_id |
250 |
|
) |
251 |
|
) |
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 |
if not await self.libjuju.model_exists(model_name): |
280 |
0 |
cloud = cloud_name or self.cloud |
281 |
0 |
credential = credential_name or cloud_name if cloud_name else self.cloud |
282 |
0 |
await self.libjuju.add_model( |
283 |
|
model_name, |
284 |
|
cloud_name=cloud, |
285 |
|
credential_name=credential |
286 |
|
) |
287 |
0 |
machine, new = await self.libjuju.create_machine( |
288 |
|
model_name=model_name, |
289 |
|
machine_id=machine_id, |
290 |
|
db_dict=db_dict, |
291 |
|
progress_timeout=progress_timeout, |
292 |
|
total_timeout=total_timeout, |
293 |
|
) |
294 |
|
# id for the execution environment |
295 |
0 |
ee_id = N2VCJujuConnector._build_ee_id( |
296 |
|
model_name=model_name, |
297 |
|
application_name=application_name, |
298 |
|
machine_id=str(machine.entity_id), |
299 |
|
) |
300 |
0 |
self.log.debug("ee_id: {}".format(ee_id)) |
301 |
|
|
302 |
0 |
if new: |
303 |
|
# write ee_id in database |
304 |
0 |
self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) |
305 |
|
|
306 |
0 |
except Exception as e: |
307 |
0 |
message = "Error creating machine on juju: {}".format(e) |
308 |
0 |
self.log.error(message) |
309 |
0 |
raise N2VCException(message=message) |
310 |
|
|
311 |
|
# new machine credentials |
312 |
0 |
credentials = { |
313 |
|
"hostname": machine.dns_name, |
314 |
|
} |
315 |
|
|
316 |
0 |
self.log.info( |
317 |
|
"Execution environment created. ee_id: {}, credentials: {}".format( |
318 |
|
ee_id, credentials |
319 |
|
) |
320 |
|
) |
321 |
|
|
322 |
0 |
return ee_id, credentials |
323 |
|
|
324 |
1 |
async def register_execution_environment( |
325 |
|
self, |
326 |
|
namespace: str, |
327 |
|
credentials: dict, |
328 |
|
db_dict: dict, |
329 |
|
progress_timeout: float = None, |
330 |
|
total_timeout: float = None, |
331 |
|
cloud_name: str = None, |
332 |
|
credential_name: str = None, |
333 |
|
) -> str: |
334 |
|
|
335 |
0 |
self.log.info( |
336 |
|
"Registering execution environment. namespace={}, credentials={}".format( |
337 |
|
namespace, credentials |
338 |
|
) |
339 |
|
) |
340 |
|
|
341 |
0 |
if credentials is None: |
342 |
0 |
raise N2VCBadArgumentsException( |
343 |
|
message="credentials are mandatory", bad_args=["credentials"] |
344 |
|
) |
345 |
0 |
if credentials.get("hostname"): |
346 |
0 |
hostname = credentials["hostname"] |
347 |
|
else: |
348 |
0 |
raise N2VCBadArgumentsException( |
349 |
|
message="hostname is mandatory", bad_args=["credentials.hostname"] |
350 |
|
) |
351 |
0 |
if credentials.get("username"): |
352 |
0 |
username = credentials["username"] |
353 |
|
else: |
354 |
0 |
raise N2VCBadArgumentsException( |
355 |
|
message="username is mandatory", bad_args=["credentials.username"] |
356 |
|
) |
357 |
0 |
if "private_key_path" in credentials: |
358 |
0 |
private_key_path = credentials["private_key_path"] |
359 |
|
else: |
360 |
|
# if not passed as argument, use generated private key path |
361 |
0 |
private_key_path = self.private_key_path |
362 |
|
|
363 |
0 |
_nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( |
364 |
|
namespace=namespace |
365 |
|
) |
366 |
|
|
367 |
|
# model name |
368 |
0 |
model_name = ns_id |
369 |
|
# application name |
370 |
0 |
application_name = self._get_application_name(namespace=namespace) |
371 |
|
|
372 |
|
# register machine on juju |
373 |
0 |
try: |
374 |
0 |
if not await self.libjuju.model_exists(model_name): |
375 |
0 |
cloud = cloud_name or self.cloud |
376 |
0 |
credential = credential_name or cloud_name if cloud_name else self.cloud |
377 |
0 |
await self.libjuju.add_model( |
378 |
|
model_name, |
379 |
|
cloud_name=cloud, |
380 |
|
credential_name=credential |
381 |
|
) |
382 |
0 |
machine_id = await self.libjuju.provision_machine( |
383 |
|
model_name=model_name, |
384 |
|
hostname=hostname, |
385 |
|
username=username, |
386 |
|
private_key_path=private_key_path, |
387 |
|
db_dict=db_dict, |
388 |
|
progress_timeout=progress_timeout, |
389 |
|
total_timeout=total_timeout, |
390 |
|
) |
391 |
0 |
except Exception as e: |
392 |
0 |
self.log.error("Error registering machine: {}".format(e)) |
393 |
0 |
raise N2VCException( |
394 |
|
message="Error registering machine on juju: {}".format(e) |
395 |
|
) |
396 |
|
|
397 |
0 |
self.log.info("Machine registered: {}".format(machine_id)) |
398 |
|
|
399 |
|
# id for the execution environment |
400 |
0 |
ee_id = N2VCJujuConnector._build_ee_id( |
401 |
|
model_name=model_name, |
402 |
|
application_name=application_name, |
403 |
|
machine_id=str(machine_id), |
404 |
|
) |
405 |
|
|
406 |
0 |
self.log.info("Execution environment registered. ee_id: {}".format(ee_id)) |
407 |
|
|
408 |
0 |
return ee_id |
409 |
|
|
410 |
1 |
async def install_configuration_sw( |
411 |
|
self, |
412 |
|
ee_id: str, |
413 |
|
artifact_path: str, |
414 |
|
db_dict: dict, |
415 |
|
progress_timeout: float = None, |
416 |
|
total_timeout: float = None, |
417 |
|
config: dict = None, |
418 |
|
num_units: int = 1, |
419 |
|
): |
420 |
|
|
421 |
0 |
self.log.info( |
422 |
|
( |
423 |
|
"Installing configuration sw on ee_id: {}, " |
424 |
|
"artifact path: {}, db_dict: {}" |
425 |
|
).format(ee_id, artifact_path, db_dict) |
426 |
|
) |
427 |
|
|
428 |
|
# check arguments |
429 |
0 |
if ee_id is None or len(ee_id) == 0: |
430 |
0 |
raise N2VCBadArgumentsException( |
431 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
432 |
|
) |
433 |
0 |
if artifact_path is None or len(artifact_path) == 0: |
434 |
0 |
raise N2VCBadArgumentsException( |
435 |
|
message="artifact_path is mandatory", bad_args=["artifact_path"] |
436 |
|
) |
437 |
0 |
if db_dict is None: |
438 |
0 |
raise N2VCBadArgumentsException( |
439 |
|
message="db_dict is mandatory", bad_args=["db_dict"] |
440 |
|
) |
441 |
|
|
442 |
0 |
try: |
443 |
0 |
( |
444 |
|
model_name, |
445 |
|
application_name, |
446 |
|
machine_id, |
447 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
448 |
0 |
self.log.debug( |
449 |
|
"model: {}, application: {}, machine: {}".format( |
450 |
|
model_name, application_name, machine_id |
451 |
|
) |
452 |
|
) |
453 |
0 |
except Exception: |
454 |
0 |
raise N2VCBadArgumentsException( |
455 |
|
message="ee_id={} is not a valid execution environment id".format( |
456 |
|
ee_id |
457 |
|
), |
458 |
|
bad_args=["ee_id"], |
459 |
|
) |
460 |
|
|
461 |
|
# remove // in charm path |
462 |
0 |
while artifact_path.find("//") >= 0: |
463 |
0 |
artifact_path = artifact_path.replace("//", "/") |
464 |
|
|
465 |
|
# check charm path |
466 |
0 |
if not self.fs.file_exists(artifact_path, mode="dir"): |
467 |
0 |
msg = "artifact path does not exist: {}".format(artifact_path) |
468 |
0 |
raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"]) |
469 |
|
|
470 |
0 |
if artifact_path.startswith("/"): |
471 |
0 |
full_path = self.fs.path + artifact_path |
472 |
|
else: |
473 |
0 |
full_path = self.fs.path + "/" + artifact_path |
474 |
|
|
475 |
0 |
try: |
476 |
0 |
await self.libjuju.deploy_charm( |
477 |
|
model_name=model_name, |
478 |
|
application_name=application_name, |
479 |
|
path=full_path, |
480 |
|
machine_id=machine_id, |
481 |
|
db_dict=db_dict, |
482 |
|
progress_timeout=progress_timeout, |
483 |
|
total_timeout=total_timeout, |
484 |
|
config=config, |
485 |
|
num_units=num_units, |
486 |
|
) |
487 |
0 |
except Exception as e: |
488 |
0 |
raise N2VCException( |
489 |
|
message="Error desploying charm into ee={} : {}".format(ee_id, e) |
490 |
|
) |
491 |
|
|
492 |
0 |
self.log.info("Configuration sw installed") |
493 |
|
|
494 |
1 |
async def install_k8s_proxy_charm( |
495 |
|
self, |
496 |
|
charm_name: str, |
497 |
|
namespace: str, |
498 |
|
artifact_path: str, |
499 |
|
db_dict: dict, |
500 |
|
progress_timeout: float = None, |
501 |
|
total_timeout: float = None, |
502 |
|
config: dict = None, |
503 |
|
cloud_name: str = None, |
504 |
|
credential_name: str = None, |
505 |
|
) -> str: |
506 |
|
""" |
507 |
|
Install a k8s proxy charm |
508 |
|
|
509 |
|
:param charm_name: Name of the charm being deployed |
510 |
|
:param namespace: collection of all the uuids related to the charm. |
511 |
|
:param str artifact_path: where to locate the artifacts (parent folder) using |
512 |
|
the self.fs |
513 |
|
the final artifact path will be a combination of this artifact_path and |
514 |
|
additional string from the config_dict (e.g. charm name) |
515 |
|
:param dict db_dict: where to write into database when the status changes. |
516 |
|
It contains a dict with |
517 |
|
{collection: <str>, filter: {}, path: <str>}, |
518 |
|
e.g. {collection: "nsrs", filter: |
519 |
|
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"} |
520 |
|
:param float progress_timeout: |
521 |
|
:param float total_timeout: |
522 |
|
:param config: Dictionary with additional configuration |
523 |
|
:param cloud_name: Cloud Name in which the charms will be deployed |
524 |
|
:param credential_name: Credential Name to use in the cloud_name. |
525 |
|
If not set, cloud_name will be used as credential_name |
526 |
|
|
527 |
|
:returns ee_id: execution environment id. |
528 |
|
""" |
529 |
1 |
self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}' |
530 |
|
.format(charm_name, artifact_path, db_dict)) |
531 |
|
|
532 |
1 |
if not self.k8s_cloud: |
533 |
1 |
raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available") |
534 |
|
|
535 |
1 |
if artifact_path is None or len(artifact_path) == 0: |
536 |
1 |
raise N2VCBadArgumentsException( |
537 |
|
message="artifact_path is mandatory", bad_args=["artifact_path"] |
538 |
|
) |
539 |
1 |
if db_dict is None: |
540 |
1 |
raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict']) |
541 |
|
|
542 |
|
# remove // in charm path |
543 |
1 |
while artifact_path.find('//') >= 0: |
544 |
1 |
artifact_path = artifact_path.replace('//', '/') |
545 |
|
|
546 |
|
# check charm path |
547 |
1 |
if not self.fs.file_exists(artifact_path, mode="dir"): |
548 |
1 |
msg = 'artifact path does not exist: {}'.format(artifact_path) |
549 |
1 |
raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path']) |
550 |
|
|
551 |
1 |
if artifact_path.startswith('/'): |
552 |
1 |
full_path = self.fs.path + artifact_path |
553 |
|
else: |
554 |
1 |
full_path = self.fs.path + '/' + artifact_path |
555 |
|
|
556 |
1 |
_, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace) |
557 |
1 |
model_name = '{}-k8s'.format(ns_id) |
558 |
1 |
if not await self.libjuju.model_exists(model_name): |
559 |
1 |
cloud = cloud_name or self.k8s_cloud |
560 |
1 |
credential = credential_name or cloud_name if cloud_name else self.k8s_cloud |
561 |
1 |
await self.libjuju.add_model( |
562 |
|
model_name, |
563 |
|
cloud_name=cloud, |
564 |
|
credential_name=credential |
565 |
|
) |
566 |
1 |
application_name = self._get_application_name(namespace) |
567 |
|
|
568 |
1 |
try: |
569 |
1 |
await self.libjuju.deploy_charm( |
570 |
|
model_name=model_name, |
571 |
|
application_name=application_name, |
572 |
|
path=full_path, |
573 |
|
machine_id=None, |
574 |
|
db_dict=db_dict, |
575 |
|
progress_timeout=progress_timeout, |
576 |
|
total_timeout=total_timeout, |
577 |
|
config=config |
578 |
|
) |
579 |
1 |
except Exception as e: |
580 |
1 |
raise N2VCException(message='Error deploying charm: {}'.format(e)) |
581 |
|
|
582 |
1 |
self.log.info('K8s proxy charm installed') |
583 |
1 |
ee_id = N2VCJujuConnector._build_ee_id( |
584 |
|
model_name=model_name, |
585 |
|
application_name=application_name, |
586 |
|
machine_id="k8s", |
587 |
|
) |
588 |
|
|
589 |
1 |
self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) |
590 |
|
|
591 |
1 |
return ee_id |
592 |
|
|
593 |
1 |
async def get_ee_ssh_public__key( |
594 |
|
self, |
595 |
|
ee_id: str, |
596 |
|
db_dict: dict, |
597 |
|
progress_timeout: float = None, |
598 |
|
total_timeout: float = None, |
599 |
|
) -> str: |
600 |
|
|
601 |
0 |
self.log.info( |
602 |
|
( |
603 |
|
"Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}" |
604 |
|
).format(ee_id, db_dict) |
605 |
|
) |
606 |
|
|
607 |
|
# check arguments |
608 |
0 |
if ee_id is None or len(ee_id) == 0: |
609 |
0 |
raise N2VCBadArgumentsException( |
610 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
611 |
|
) |
612 |
0 |
if db_dict is None: |
613 |
0 |
raise N2VCBadArgumentsException( |
614 |
|
message="db_dict is mandatory", bad_args=["db_dict"] |
615 |
|
) |
616 |
|
|
617 |
0 |
try: |
618 |
0 |
( |
619 |
|
model_name, |
620 |
|
application_name, |
621 |
|
machine_id, |
622 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
623 |
0 |
self.log.debug( |
624 |
|
"model: {}, application: {}, machine: {}".format( |
625 |
|
model_name, application_name, machine_id |
626 |
|
) |
627 |
|
) |
628 |
0 |
except Exception: |
629 |
0 |
raise N2VCBadArgumentsException( |
630 |
|
message="ee_id={} is not a valid execution environment id".format( |
631 |
|
ee_id |
632 |
|
), |
633 |
|
bad_args=["ee_id"], |
634 |
|
) |
635 |
|
|
636 |
|
# try to execute ssh layer primitives (if exist): |
637 |
|
# generate-ssh-key |
638 |
|
# get-ssh-public-key |
639 |
|
|
640 |
0 |
output = None |
641 |
|
|
642 |
0 |
application_name = N2VCJujuConnector._format_app_name(application_name) |
643 |
|
|
644 |
|
# execute action: generate-ssh-key |
645 |
0 |
try: |
646 |
0 |
output, _status = await self.libjuju.execute_action( |
647 |
|
model_name=model_name, |
648 |
|
application_name=application_name, |
649 |
|
action_name="generate-ssh-key", |
650 |
|
db_dict=db_dict, |
651 |
|
progress_timeout=progress_timeout, |
652 |
|
total_timeout=total_timeout, |
653 |
|
) |
654 |
0 |
except Exception as e: |
655 |
0 |
self.log.info( |
656 |
|
"Skipping exception while executing action generate-ssh-key: {}".format( |
657 |
|
e |
658 |
|
) |
659 |
|
) |
660 |
|
|
661 |
|
# execute action: get-ssh-public-key |
662 |
0 |
try: |
663 |
0 |
output, _status = await self.libjuju.execute_action( |
664 |
|
model_name=model_name, |
665 |
|
application_name=application_name, |
666 |
|
action_name="get-ssh-public-key", |
667 |
|
db_dict=db_dict, |
668 |
|
progress_timeout=progress_timeout, |
669 |
|
total_timeout=total_timeout, |
670 |
|
) |
671 |
0 |
except Exception as e: |
672 |
0 |
msg = "Cannot execute action get-ssh-public-key: {}\n".format(e) |
673 |
0 |
self.log.info(msg) |
674 |
0 |
raise N2VCExecutionException(e, primitive_name="get-ssh-public-key") |
675 |
|
|
676 |
|
# return public key if exists |
677 |
0 |
return output["pubkey"] if "pubkey" in output else output |
678 |
|
|
679 |
1 |
async def get_metrics(self, model_name: str, application_name: str) -> dict: |
680 |
1 |
return await self.libjuju.get_metrics(model_name, application_name) |
681 |
|
|
682 |
1 |
async def add_relation( |
683 |
|
self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str |
684 |
|
): |
685 |
|
|
686 |
0 |
self.log.debug( |
687 |
|
"adding new relation between {} and {}, endpoints: {}, {}".format( |
688 |
|
ee_id_1, ee_id_2, endpoint_1, endpoint_2 |
689 |
|
) |
690 |
|
) |
691 |
|
|
692 |
|
# check arguments |
693 |
0 |
if not ee_id_1: |
694 |
0 |
message = "EE 1 is mandatory" |
695 |
0 |
self.log.error(message) |
696 |
0 |
raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"]) |
697 |
0 |
if not ee_id_2: |
698 |
0 |
message = "EE 2 is mandatory" |
699 |
0 |
self.log.error(message) |
700 |
0 |
raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"]) |
701 |
0 |
if not endpoint_1: |
702 |
0 |
message = "endpoint 1 is mandatory" |
703 |
0 |
self.log.error(message) |
704 |
0 |
raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"]) |
705 |
0 |
if not endpoint_2: |
706 |
0 |
message = "endpoint 2 is mandatory" |
707 |
0 |
self.log.error(message) |
708 |
0 |
raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"]) |
709 |
|
|
710 |
|
# get the model, the applications and the machines from the ee_id's |
711 |
0 |
model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1) |
712 |
0 |
model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2) |
713 |
|
|
714 |
|
# model must be the same |
715 |
0 |
if model_1 != model_2: |
716 |
0 |
message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2) |
717 |
0 |
self.log.error(message) |
718 |
0 |
raise N2VCBadArgumentsException( |
719 |
|
message=message, bad_args=["ee_id_1", "ee_id_2"] |
720 |
|
) |
721 |
|
|
722 |
|
# add juju relations between two applications |
723 |
0 |
try: |
724 |
0 |
await self.libjuju.add_relation( |
725 |
|
model_name=model_1, |
726 |
|
endpoint_1="{}:{}".format(app_1, endpoint_1), |
727 |
|
endpoint_2="{}:{}".format(app_2, endpoint_2), |
728 |
|
) |
729 |
0 |
except Exception as e: |
730 |
0 |
message = "Error adding relation between {} and {}: {}".format( |
731 |
|
ee_id_1, ee_id_2, e |
732 |
|
) |
733 |
0 |
self.log.error(message) |
734 |
0 |
raise N2VCException(message=message) |
735 |
|
|
736 |
1 |
async def remove_relation(self): |
737 |
|
# TODO |
738 |
0 |
self.log.info("Method not implemented yet") |
739 |
0 |
raise MethodNotImplemented() |
740 |
|
|
741 |
1 |
async def deregister_execution_environments(self): |
742 |
0 |
self.log.info("Method not implemented yet") |
743 |
0 |
raise MethodNotImplemented() |
744 |
|
|
745 |
1 |
async def delete_namespace( |
746 |
|
self, namespace: str, db_dict: dict = None, total_timeout: float = None |
747 |
|
): |
748 |
0 |
self.log.info("Deleting namespace={}".format(namespace)) |
749 |
|
|
750 |
|
# check arguments |
751 |
0 |
if namespace is None: |
752 |
0 |
raise N2VCBadArgumentsException( |
753 |
|
message="namespace is mandatory", bad_args=["namespace"] |
754 |
|
) |
755 |
|
|
756 |
0 |
_nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( |
757 |
|
namespace=namespace |
758 |
|
) |
759 |
0 |
if ns_id is not None: |
760 |
0 |
try: |
761 |
0 |
models = await self.libjuju.list_models(contains=ns_id) |
762 |
0 |
for model in models: |
763 |
0 |
await self.libjuju.destroy_model( |
764 |
|
model_name=model, total_timeout=total_timeout |
765 |
|
) |
766 |
0 |
except Exception as e: |
767 |
0 |
raise N2VCException( |
768 |
|
message="Error deleting namespace {} : {}".format(namespace, e) |
769 |
|
) |
770 |
|
else: |
771 |
0 |
raise N2VCBadArgumentsException( |
772 |
|
message="only ns_id is permitted to delete yet", bad_args=["namespace"] |
773 |
|
) |
774 |
|
|
775 |
0 |
self.log.info("Namespace {} deleted".format(namespace)) |
776 |
|
|
777 |
1 |
async def delete_execution_environment( |
778 |
|
self, ee_id: str, db_dict: dict = None, total_timeout: float = None, |
779 |
|
scaling_in: bool = False |
780 |
|
): |
781 |
0 |
self.log.info("Deleting execution environment ee_id={}".format(ee_id)) |
782 |
|
|
783 |
|
# check arguments |
784 |
0 |
if ee_id is None: |
785 |
0 |
raise N2VCBadArgumentsException( |
786 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
787 |
|
) |
788 |
|
|
789 |
0 |
model_name, application_name, _machine_id = self._get_ee_id_components( |
790 |
|
ee_id=ee_id |
791 |
|
) |
792 |
0 |
try: |
793 |
0 |
if not scaling_in: |
794 |
|
# destroy the model |
795 |
|
# TODO: should this be removed? |
796 |
0 |
await self.libjuju.destroy_model( |
797 |
|
model_name=model_name, total_timeout=total_timeout |
798 |
|
) |
799 |
|
else: |
800 |
|
# get juju model and observer |
801 |
0 |
controller = await self.libjuju.get_controller() |
802 |
0 |
model = await self.libjuju.get_model(controller, model_name) |
803 |
|
# destroy the application |
804 |
0 |
await self.libjuju.destroy_application( |
805 |
|
model=model, application_name=application_name) |
806 |
0 |
except Exception as e: |
807 |
0 |
raise N2VCException( |
808 |
|
message=( |
809 |
|
"Error deleting execution environment {} (application {}) : {}" |
810 |
|
).format(ee_id, application_name, e) |
811 |
|
) |
812 |
|
|
813 |
0 |
self.log.info("Execution environment {} deleted".format(ee_id)) |
814 |
|
|
815 |
1 |
async def exec_primitive( |
816 |
|
self, |
817 |
|
ee_id: str, |
818 |
|
primitive_name: str, |
819 |
|
params_dict: dict, |
820 |
|
db_dict: dict = None, |
821 |
|
progress_timeout: float = None, |
822 |
|
total_timeout: float = None, |
823 |
|
) -> str: |
824 |
|
|
825 |
0 |
self.log.info( |
826 |
|
"Executing primitive: {} on ee: {}, params: {}".format( |
827 |
|
primitive_name, ee_id, params_dict |
828 |
|
) |
829 |
|
) |
830 |
|
|
831 |
|
# check arguments |
832 |
0 |
if ee_id is None or len(ee_id) == 0: |
833 |
0 |
raise N2VCBadArgumentsException( |
834 |
|
message="ee_id is mandatory", bad_args=["ee_id"] |
835 |
|
) |
836 |
0 |
if primitive_name is None or len(primitive_name) == 0: |
837 |
0 |
raise N2VCBadArgumentsException( |
838 |
|
message="action_name is mandatory", bad_args=["action_name"] |
839 |
|
) |
840 |
0 |
if params_dict is None: |
841 |
0 |
params_dict = dict() |
842 |
|
|
843 |
0 |
try: |
844 |
0 |
( |
845 |
|
model_name, |
846 |
|
application_name, |
847 |
|
_machine_id, |
848 |
|
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) |
849 |
0 |
except Exception: |
850 |
0 |
raise N2VCBadArgumentsException( |
851 |
|
message="ee_id={} is not a valid execution environment id".format( |
852 |
|
ee_id |
853 |
|
), |
854 |
|
bad_args=["ee_id"], |
855 |
|
) |
856 |
|
|
857 |
0 |
if primitive_name == "config": |
858 |
|
# Special case: config primitive |
859 |
0 |
try: |
860 |
0 |
await self.libjuju.configure_application( |
861 |
|
model_name=model_name, |
862 |
|
application_name=application_name, |
863 |
|
config=params_dict, |
864 |
|
) |
865 |
0 |
actions = await self.libjuju.get_actions( |
866 |
|
application_name=application_name, model_name=model_name, |
867 |
|
) |
868 |
0 |
self.log.debug( |
869 |
|
"Application {} has these actions: {}".format( |
870 |
|
application_name, actions |
871 |
|
) |
872 |
|
) |
873 |
0 |
if "verify-ssh-credentials" in actions: |
874 |
|
# execute verify-credentials |
875 |
0 |
num_retries = 20 |
876 |
0 |
retry_timeout = 15.0 |
877 |
0 |
for _ in range(num_retries): |
878 |
0 |
try: |
879 |
0 |
self.log.debug("Executing action verify-ssh-credentials...") |
880 |
0 |
output, ok = await self.libjuju.execute_action( |
881 |
|
model_name=model_name, |
882 |
|
application_name=application_name, |
883 |
|
action_name="verify-ssh-credentials", |
884 |
|
db_dict=db_dict, |
885 |
|
progress_timeout=progress_timeout, |
886 |
|
total_timeout=total_timeout, |
887 |
|
) |
888 |
|
|
889 |
0 |
if ok == "failed": |
890 |
0 |
self.log.debug( |
891 |
|
"Error executing verify-ssh-credentials: {}. Retrying..." |
892 |
|
) |
893 |
0 |
await asyncio.sleep(retry_timeout) |
894 |
|
|
895 |
0 |
continue |
896 |
0 |
self.log.debug("Result: {}, output: {}".format(ok, output)) |
897 |
0 |
break |
898 |
0 |
except asyncio.CancelledError: |
899 |
0 |
raise |
900 |
|
else: |
901 |
0 |
self.log.error( |
902 |
|
"Error executing verify-ssh-credentials after {} retries. ".format( |
903 |
|
num_retries |
904 |
|
) |
905 |
|
) |
906 |
|
else: |
907 |
0 |
msg = "Action verify-ssh-credentials does not exist in application {}".format( |
908 |
|
application_name |
909 |
|
) |
910 |
0 |
self.log.debug(msg=msg) |
911 |
0 |
except Exception as e: |
912 |
0 |
self.log.error("Error configuring juju application: {}".format(e)) |
913 |
0 |
raise N2VCExecutionException( |
914 |
|
message="Error configuring application into ee={} : {}".format( |
915 |
|
ee_id, e |
916 |
|
), |
917 |
|
primitive_name=primitive_name, |
918 |
|
) |
919 |
0 |
return "CONFIG OK" |
920 |
|
else: |
921 |
0 |
try: |
922 |
0 |
output, status = await self.libjuju.execute_action( |
923 |
|
model_name=model_name, |
924 |
|
application_name=application_name, |
925 |
|
action_name=primitive_name, |
926 |
|
db_dict=db_dict, |
927 |
|
progress_timeout=progress_timeout, |
928 |
|
total_timeout=total_timeout, |
929 |
|
**params_dict |
930 |
|
) |
931 |
0 |
if status == "completed": |
932 |
0 |
return output |
933 |
|
else: |
934 |
0 |
raise Exception("status is not completed: {}".format(status)) |
935 |
0 |
except Exception as e: |
936 |
0 |
self.log.error( |
937 |
|
"Error executing primitive {}: {}".format(primitive_name, e) |
938 |
|
) |
939 |
0 |
raise N2VCExecutionException( |
940 |
|
message="Error executing primitive {} into ee={} : {}".format( |
941 |
|
primitive_name, ee_id, e |
942 |
|
), |
943 |
|
primitive_name=primitive_name, |
944 |
|
) |
945 |
|
|
946 |
1 |
async def disconnect(self): |
947 |
0 |
self.log.info("closing juju N2VC...") |
948 |
0 |
try: |
949 |
0 |
await self.libjuju.disconnect() |
950 |
0 |
except Exception as e: |
951 |
0 |
raise N2VCConnectionException( |
952 |
|
message="Error disconnecting controller: {}".format(e), url=self.url |
953 |
|
) |
954 |
|
|
955 |
|
""" |
956 |
|
#################################################################################### |
957 |
|
################################### P R I V A T E ################################## |
958 |
|
#################################################################################### |
959 |
|
""" |
960 |
|
|
961 |
1 |
def _write_ee_id_db(self, db_dict: dict, ee_id: str): |
962 |
|
|
963 |
|
# write ee_id to database: _admin.deployed.VCA.x |
964 |
1 |
try: |
965 |
1 |
the_table = db_dict["collection"] |
966 |
0 |
the_filter = db_dict["filter"] |
967 |
0 |
the_path = db_dict["path"] |
968 |
0 |
if not the_path[-1] == ".": |
969 |
0 |
the_path = the_path + "." |
970 |
0 |
update_dict = {the_path + "ee_id": ee_id} |
971 |
|
# self.log.debug('Writing ee_id to database: {}'.format(the_path)) |
972 |
0 |
self.db.set_one( |
973 |
|
table=the_table, |
974 |
|
q_filter=the_filter, |
975 |
|
update_dict=update_dict, |
976 |
|
fail_on_empty=True, |
977 |
|
) |
978 |
1 |
except asyncio.CancelledError: |
979 |
0 |
raise |
980 |
1 |
except Exception as e: |
981 |
1 |
self.log.error("Error writing ee_id to database: {}".format(e)) |
982 |
|
|
983 |
1 |
@staticmethod |
984 |
1 |
def _build_ee_id(model_name: str, application_name: str, machine_id: str): |
985 |
|
""" |
986 |
|
Build an execution environment id form model, application and machine |
987 |
|
:param model_name: |
988 |
|
:param application_name: |
989 |
|
:param machine_id: |
990 |
|
:return: |
991 |
|
""" |
992 |
|
# id for the execution environment |
993 |
1 |
return "{}.{}.{}".format(model_name, application_name, machine_id) |
994 |
|
|
995 |
1 |
@staticmethod |
996 |
1 |
def _get_ee_id_components(ee_id: str) -> (str, str, str): |
997 |
|
""" |
998 |
|
Get model, application and machine components from an execution environment id |
999 |
|
:param ee_id: |
1000 |
|
:return: model_name, application_name, machine_id |
1001 |
|
""" |
1002 |
|
|
1003 |
0 |
if ee_id is None: |
1004 |
0 |
return None, None, None |
1005 |
|
|
1006 |
|
# split components of id |
1007 |
0 |
parts = ee_id.split(".") |
1008 |
0 |
model_name = parts[0] |
1009 |
0 |
application_name = parts[1] |
1010 |
0 |
machine_id = parts[2] |
1011 |
0 |
return model_name, application_name, machine_id |
1012 |
|
|
1013 |
1 |
def _get_application_name(self, namespace: str) -> str: |
1014 |
|
""" |
1015 |
|
Build application name from namespace |
1016 |
|
:param namespace: |
1017 |
|
:return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count> |
1018 |
|
""" |
1019 |
|
|
1020 |
|
# TODO: Enforce the Juju 50-character application limit |
1021 |
|
|
1022 |
|
# split namespace components |
1023 |
1 |
_, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components( |
1024 |
|
namespace=namespace |
1025 |
|
) |
1026 |
|
|
1027 |
1 |
if vnf_id is None or len(vnf_id) == 0: |
1028 |
0 |
vnf_id = "" |
1029 |
|
else: |
1030 |
|
# Shorten the vnf_id to its last twelve characters |
1031 |
1 |
vnf_id = "vnf-" + vnf_id[-12:] |
1032 |
|
|
1033 |
1 |
if vdu_id is None or len(vdu_id) == 0: |
1034 |
0 |
vdu_id = "" |
1035 |
|
else: |
1036 |
|
# Shorten the vdu_id to its last twelve characters |
1037 |
1 |
vdu_id = "-vdu-" + vdu_id[-12:] |
1038 |
|
|
1039 |
1 |
if vdu_count is None or len(vdu_count) == 0: |
1040 |
1 |
vdu_count = "" |
1041 |
|
else: |
1042 |
0 |
vdu_count = "-cnt-" + vdu_count |
1043 |
|
|
1044 |
1 |
application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count) |
1045 |
|
|
1046 |
1 |
return N2VCJujuConnector._format_app_name(application_name) |
1047 |
|
|
1048 |
1 |
def _create_juju_public_key(self): |
1049 |
|
"""Recreate the Juju public key on lcm container, if needed |
1050 |
|
Certain libjuju commands expect to be run from the same machine as Juju |
1051 |
|
is bootstrapped to. This method will write the public key to disk in |
1052 |
|
that location: ~/.local/share/juju/ssh/juju_id_rsa.pub |
1053 |
|
""" |
1054 |
|
|
1055 |
|
# Make sure that we have a public key before writing to disk |
1056 |
1 |
if self.public_key is None or len(self.public_key) == 0: |
1057 |
1 |
if "OSMLCM_VCA_PUBKEY" in os.environ: |
1058 |
0 |
self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "") |
1059 |
0 |
if len(self.public_key) == 0: |
1060 |
0 |
return |
1061 |
|
else: |
1062 |
1 |
return |
1063 |
|
|
1064 |
0 |
pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~")) |
1065 |
0 |
file_path = "{}/juju_id_rsa.pub".format(pk_path) |
1066 |
0 |
self.log.debug( |
1067 |
|
"writing juju public key to file:\n{}\npublic key: {}".format( |
1068 |
|
file_path, self.public_key |
1069 |
|
) |
1070 |
|
) |
1071 |
0 |
if not os.path.exists(pk_path): |
1072 |
|
# create path and write file |
1073 |
0 |
os.makedirs(pk_path) |
1074 |
0 |
with open(file_path, "w") as f: |
1075 |
0 |
self.log.debug("Creating juju public key file: {}".format(file_path)) |
1076 |
0 |
f.write(self.public_key) |
1077 |
|
else: |
1078 |
0 |
self.log.debug("juju public key file already exists: {}".format(file_path)) |
1079 |
|
|
1080 |
1 |
@staticmethod |
1081 |
1 |
def _format_model_name(name: str) -> str: |
1082 |
|
"""Format the name of the model. |
1083 |
|
|
1084 |
|
Model names may only contain lowercase letters, digits and hyphens |
1085 |
|
""" |
1086 |
|
|
1087 |
0 |
return name.replace("_", "-").replace(" ", "-").lower() |
1088 |
|
|
1089 |
1 |
@staticmethod |
1090 |
1 |
def _format_app_name(name: str) -> str: |
1091 |
|
"""Format the name of the application (in order to assure valid application name). |
1092 |
|
|
1093 |
|
Application names have restrictions (run juju deploy --help): |
1094 |
|
- contains lowercase letters 'a'-'z' |
1095 |
|
- contains numbers '0'-'9' |
1096 |
|
- contains hyphens '-' |
1097 |
|
- starts with a lowercase letter |
1098 |
|
- not two or more consecutive hyphens |
1099 |
|
- after a hyphen, not a group with all numbers |
1100 |
|
""" |
1101 |
|
|
1102 |
1 |
def all_numbers(s: str) -> bool: |
1103 |
1 |
for c in s: |
1104 |
1 |
if not c.isdigit(): |
1105 |
1 |
return False |
1106 |
0 |
return True |
1107 |
|
|
1108 |
1 |
new_name = name.replace("_", "-") |
1109 |
1 |
new_name = new_name.replace(" ", "-") |
1110 |
1 |
new_name = new_name.lower() |
1111 |
1 |
while new_name.find("--") >= 0: |
1112 |
0 |
new_name = new_name.replace("--", "-") |
1113 |
1 |
groups = new_name.split("-") |
1114 |
|
|
1115 |
|
# find 'all numbers' groups and prefix them with a letter |
1116 |
1 |
app_name = "" |
1117 |
1 |
for i in range(len(groups)): |
1118 |
1 |
group = groups[i] |
1119 |
1 |
if all_numbers(group): |
1120 |
0 |
group = "z" + group |
1121 |
1 |
if i > 0: |
1122 |
1 |
app_name += "-" |
1123 |
1 |
app_name += group |
1124 |
|
|
1125 |
1 |
if app_name[0].isdigit(): |
1126 |
0 |
app_name = "z" + app_name |
1127 |
|
|
1128 |
1 |
return app_name |