Get series from machine
[osm/N2VC.git] / n2vc / n2vc_juju_conn.py
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 import asyncio
24 import base64
25 import binascii
26 import logging
27 import os
28 import re
29 import time
30
31 from juju.action import Action
32 from juju.application import Application
33 from juju.client import client
34 from juju.controller import Controller
35 from juju.errors import JujuAPIError
36 from juju.machine import Machine
37 from juju.model import Model
38 from n2vc.exceptions import (
39 N2VCBadArgumentsException,
40 N2VCException,
41 N2VCConnectionException,
42 N2VCExecutionException,
43 N2VCInvalidCertificate,
44 N2VCNotFound,
45 MethodNotImplemented,
46 )
47 from n2vc.juju_observer import JujuModelObserver
48 from n2vc.n2vc_conn import N2VCConnector
49 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
50 from n2vc.provisioner import AsyncSSHProvisioner
51
52
53 class N2VCJujuConnector(N2VCConnector):
54
55 """
56 ####################################################################################
57 ################################### P U B L I C ####################################
58 ####################################################################################
59 """
60
61 BUILT_IN_CLOUDS = ["localhost", "microk8s"]
62
63 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 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 logging.getLogger("websockets.protocol").setLevel(logging.INFO)
92 logging.getLogger("juju.client.connection").setLevel(logging.WARN)
93 logging.getLogger("model").setLevel(logging.WARN)
94
95 self.log.info("Initializing N2VC juju connector...")
96
97 """
98 ##############################################################
99 # check arguments
100 ##############################################################
101 """
102
103 # juju URL
104 if url is None:
105 raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
106 url_parts = url.split(":")
107 if len(url_parts) != 2:
108 raise N2VCBadArgumentsException(
109 "Argument url: bad format (localhost:port) -> {}".format(url), ["url"]
110 )
111 self.hostname = url_parts[0]
112 try:
113 self.port = int(url_parts[1])
114 except ValueError:
115 raise N2VCBadArgumentsException(
116 "url port must be a number -> {}".format(url), ["url"]
117 )
118
119 # juju USERNAME
120 if username is None:
121 raise N2VCBadArgumentsException(
122 "Argument username is mandatory", ["username"]
123 )
124
125 # juju CONFIGURATION
126 if vca_config is None:
127 raise N2VCBadArgumentsException(
128 "Argument vca_config is mandatory", ["vca_config"]
129 )
130
131 if "secret" in vca_config:
132 self.secret = vca_config["secret"]
133 else:
134 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 if "public_key" in vca_config:
141 self.public_key = vca_config["public_key"]
142 else:
143 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 def base64_to_cacert(b64string):
148 """Convert the base64-encoded string containing the VCA CACERT.
149
150 The input string....
151
152 """
153 try:
154 cacert = base64.b64decode(b64string).decode("utf-8")
155
156 cacert = re.sub(r"\\n", r"\n", cacert,)
157 except binascii.Error as e:
158 self.log.debug("Caught binascii.Error: {}".format(e))
159 raise N2VCInvalidCertificate(message="Invalid CA Certificate")
160
161 return cacert
162
163 self.ca_cert = vca_config.get("ca_cert")
164 if self.ca_cert:
165 self.ca_cert = base64_to_cacert(vca_config["ca_cert"])
166
167 if "api_proxy" in vca_config:
168 self.api_proxy = vca_config["api_proxy"]
169 self.log.debug(
170 "api_proxy for native charms configured: {}".format(self.api_proxy)
171 )
172 else:
173 self.warning(
174 "api_proxy is not configured. Support for native charms is disabled"
175 )
176
177 if "enable_os_upgrade" in vca_config:
178 self.enable_os_upgrade = vca_config["enable_os_upgrade"]
179 else:
180 self.enable_os_upgrade = True
181
182 if "apt_mirror" in vca_config:
183 self.apt_mirror = vca_config["apt_mirror"]
184 else:
185 self.apt_mirror = None
186
187 self.cloud = vca_config.get("cloud")
188 # self.log.debug('Arguments have been checked')
189
190 # juju data
191 self.controller = None # it will be filled when connect to juju
192 self.juju_models = {} # model objects for every model_name
193 self.juju_observers = {} # model observers for every model_name
194 self._connecting = (
195 False # while connecting to juju (to avoid duplicate connections)
196 )
197 self._authenticated = (
198 False # it will be True when juju connection be stablished
199 )
200 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 self._create_juju_public_key()
205
206 self.log.info("N2VC juju connector initialized")
207
208 async def get_status(self, namespace: str, yaml_format: bool = True):
209
210 # self.log.info('Getting NS status. namespace: {}'.format(namespace))
211
212 if not self._authenticated:
213 await self._juju_login()
214
215 _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 model_name = ns_id
220 if model_name is None:
221 msg = "Namespace {} not valid".format(namespace)
222 self.log.error(msg)
223 raise N2VCBadArgumentsException(msg, ["namespace"])
224
225 # get juju model (create model if needed)
226 model = await self._juju_get_model(model_name=model_name)
227
228 status = await model.get_status()
229
230 if yaml_format:
231 return obj_to_yaml(status)
232 else:
233 return obj_to_dict(status)
234
235 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 self.log.info(
245 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
246 namespace, reuse_ee_id
247 )
248 )
249
250 if not self._authenticated:
251 await self._juju_login()
252
253 machine_id = None
254 if reuse_ee_id:
255 model_name, application_name, machine_id = self._get_ee_id_components(
256 ee_id=reuse_ee_id
257 )
258 else:
259 (
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 model_name = ns_id
268 # application name
269 application_name = self._get_application_name(namespace=namespace)
270
271 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 try:
279 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 except Exception as e:
288 message = "Error creating machine on juju: {}".format(e)
289 self.log.error(message)
290 raise N2VCException(message=message)
291
292 # id for the execution environment
293 ee_id = N2VCJujuConnector._build_ee_id(
294 model_name=model_name,
295 application_name=application_name,
296 machine_id=str(machine.entity_id),
297 )
298 self.log.debug("ee_id: {}".format(ee_id))
299
300 # new machine credentials
301 credentials = dict()
302 credentials["hostname"] = machine.dns_name
303
304 self.log.info(
305 "Execution environment created. ee_id: {}, credentials: {}".format(
306 ee_id, credentials
307 )
308 )
309
310 return ee_id, credentials
311
312 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 if not self._authenticated:
322 await self._juju_login()
323
324 self.log.info(
325 "Registering execution environment. namespace={}, credentials={}".format(
326 namespace, credentials
327 )
328 )
329
330 if credentials is None:
331 raise N2VCBadArgumentsException(
332 message="credentials are mandatory", bad_args=["credentials"]
333 )
334 if credentials.get("hostname"):
335 hostname = credentials["hostname"]
336 else:
337 raise N2VCBadArgumentsException(
338 message="hostname is mandatory", bad_args=["credentials.hostname"]
339 )
340 if credentials.get("username"):
341 username = credentials["username"]
342 else:
343 raise N2VCBadArgumentsException(
344 message="username is mandatory", bad_args=["credentials.username"]
345 )
346 if "private_key_path" in credentials:
347 private_key_path = credentials["private_key_path"]
348 else:
349 # if not passed as argument, use generated private key path
350 private_key_path = self.private_key_path
351
352 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
353 namespace=namespace
354 )
355
356 # model name
357 model_name = ns_id
358 # application name
359 application_name = self._get_application_name(namespace=namespace)
360
361 # register machine on juju
362 try:
363 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 except Exception as e:
373 self.log.error("Error registering machine: {}".format(e))
374 raise N2VCException(
375 message="Error registering machine on juju: {}".format(e)
376 )
377
378 self.log.info("Machine registered: {}".format(machine_id))
379
380 # id for the execution environment
381 ee_id = N2VCJujuConnector._build_ee_id(
382 model_name=model_name,
383 application_name=application_name,
384 machine_id=str(machine_id),
385 )
386
387 self.log.info("Execution environment registered. ee_id: {}".format(ee_id))
388
389 return ee_id
390
391 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 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 if not self._authenticated:
409 await self._juju_login()
410
411 # check arguments
412 if ee_id is None or len(ee_id) == 0:
413 raise N2VCBadArgumentsException(
414 message="ee_id is mandatory", bad_args=["ee_id"]
415 )
416 if artifact_path is None or len(artifact_path) == 0:
417 raise N2VCBadArgumentsException(
418 message="artifact_path is mandatory", bad_args=["artifact_path"]
419 )
420 if db_dict is None:
421 raise N2VCBadArgumentsException(
422 message="db_dict is mandatory", bad_args=["db_dict"]
423 )
424
425 try:
426 (
427 model_name,
428 application_name,
429 machine_id,
430 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
431 self.log.debug(
432 "model: {}, application: {}, machine: {}".format(
433 model_name, application_name, machine_id
434 )
435 )
436 except Exception:
437 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 while artifact_path.find("//") >= 0:
446 artifact_path = artifact_path.replace("//", "/")
447
448 # check charm path
449 if not self.fs.file_exists(artifact_path, mode="dir"):
450 msg = "artifact path does not exist: {}".format(artifact_path)
451 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
452
453 if artifact_path.startswith("/"):
454 full_path = self.fs.path + artifact_path
455 else:
456 full_path = self.fs.path + "/" + artifact_path
457
458 try:
459 await self._juju_deploy_charm(
460 model_name=model_name,
461 application_name=application_name,
462 charm_path=full_path,
463 machine_id=machine_id,
464 db_dict=db_dict,
465 progress_timeout=progress_timeout,
466 total_timeout=total_timeout,
467 config=config,
468 )
469 except Exception as e:
470 raise N2VCException(
471 message="Error desploying charm into ee={} : {}".format(ee_id, e)
472 )
473
474 self.log.info("Configuration sw installed")
475
476 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 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 if not self._authenticated:
491 await self._juju_login()
492
493 # check arguments
494 if ee_id is None or len(ee_id) == 0:
495 raise N2VCBadArgumentsException(
496 message="ee_id is mandatory", bad_args=["ee_id"]
497 )
498 if db_dict is None:
499 raise N2VCBadArgumentsException(
500 message="db_dict is mandatory", bad_args=["db_dict"]
501 )
502
503 try:
504 (
505 model_name,
506 application_name,
507 machine_id,
508 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
509 self.log.debug(
510 "model: {}, application: {}, machine: {}".format(
511 model_name, application_name, machine_id
512 )
513 )
514 except Exception:
515 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 output = None
527
528 # execute action: generate-ssh-key
529 try:
530 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 except Exception as e:
539 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 try:
547 output, _status = await self._juju_execute_action(
548 model_name=model_name,
549 application_name=application_name,
550 action_name="get-ssh-public-key",
551 db_dict=db_dict,
552 progress_timeout=progress_timeout,
553 total_timeout=total_timeout,
554 )
555 except Exception as e:
556 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e)
557 self.log.info(msg)
558 raise N2VCException(msg)
559
560 # return public key if exists
561 return output["pubkey"] if "pubkey" in output else output
562
563 async def add_relation(
564 self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
565 ):
566
567 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 if not ee_id_1:
575 message = "EE 1 is mandatory"
576 self.log.error(message)
577 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
578 if not ee_id_2:
579 message = "EE 2 is mandatory"
580 self.log.error(message)
581 raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
582 if not endpoint_1:
583 message = "endpoint 1 is mandatory"
584 self.log.error(message)
585 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
586 if not endpoint_2:
587 message = "endpoint 2 is mandatory"
588 self.log.error(message)
589 raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
590
591 if not self._authenticated:
592 await self._juju_login()
593
594 # get the model, the applications and the machines from the ee_id's
595 model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
596 model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
597
598 # model must be the same
599 if model_1 != model_2:
600 message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
601 self.log.error(message)
602 raise N2VCBadArgumentsException(
603 message=message, bad_args=["ee_id_1", "ee_id_2"]
604 )
605
606 # add juju relations between two applications
607 try:
608 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 except Exception as e:
616 message = "Error adding relation between {} and {}: {}".format(
617 ee_id_1, ee_id_2, e
618 )
619 self.log.error(message)
620 raise N2VCException(message=message)
621
622 async def remove_relation(self):
623 if not self._authenticated:
624 await self._juju_login()
625 # TODO
626 self.log.info("Method not implemented yet")
627 raise MethodNotImplemented()
628
629 async def deregister_execution_environments(self):
630 if not self._authenticated:
631 await self._juju_login()
632 # TODO
633 self.log.info("Method not implemented yet")
634 raise MethodNotImplemented()
635
636 async def delete_namespace(
637 self, namespace: str, db_dict: dict = None, total_timeout: float = None
638 ):
639 self.log.info("Deleting namespace={}".format(namespace))
640
641 if not self._authenticated:
642 await self._juju_login()
643
644 # check arguments
645 if namespace is None:
646 raise N2VCBadArgumentsException(
647 message="namespace is mandatory", bad_args=["namespace"]
648 )
649
650 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
651 namespace=namespace
652 )
653 if ns_id is not None:
654 try:
655 await self._juju_destroy_model(
656 model_name=ns_id, total_timeout=total_timeout
657 )
658 except N2VCNotFound:
659 raise
660 except Exception as e:
661 raise N2VCException(
662 message="Error deleting namespace {} : {}".format(namespace, e)
663 )
664 else:
665 raise N2VCBadArgumentsException(
666 message="only ns_id is permitted to delete yet", bad_args=["namespace"]
667 )
668
669 self.log.info("Namespace {} deleted".format(namespace))
670
671 async def delete_execution_environment(
672 self, ee_id: str, db_dict: dict = None, total_timeout: float = None
673 ):
674 self.log.info("Deleting execution environment ee_id={}".format(ee_id))
675
676 if not self._authenticated:
677 await self._juju_login()
678
679 # check arguments
680 if ee_id is None:
681 raise N2VCBadArgumentsException(
682 message="ee_id is mandatory", bad_args=["ee_id"]
683 )
684
685 model_name, application_name, _machine_id = self._get_ee_id_components(
686 ee_id=ee_id
687 )
688
689 # destroy the application
690 try:
691 await self._juju_destroy_application(
692 model_name=model_name, application_name=application_name
693 )
694 except Exception as e:
695 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 self.log.info("Execution environment {} deleted".format(ee_id))
714
715 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 self.log.info(
726 "Executing primitive: {} on ee: {}, params: {}".format(
727 primitive_name, ee_id, params_dict
728 )
729 )
730
731 if not self._authenticated:
732 await self._juju_login()
733
734 # check arguments
735 if ee_id is None or len(ee_id) == 0:
736 raise N2VCBadArgumentsException(
737 message="ee_id is mandatory", bad_args=["ee_id"]
738 )
739 if primitive_name is None or len(primitive_name) == 0:
740 raise N2VCBadArgumentsException(
741 message="action_name is mandatory", bad_args=["action_name"]
742 )
743 if params_dict is None:
744 params_dict = dict()
745
746 try:
747 (
748 model_name,
749 application_name,
750 _machine_id,
751 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
752 except Exception:
753 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 if primitive_name == "config":
761 # Special case: config primitive
762 try:
763 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 except Exception as e:
772 self.log.error("Error configuring juju application: {}".format(e))
773 raise N2VCExecutionException(
774 message="Error configuring application into ee={} : {}".format(
775 ee_id, e
776 ),
777 primitive_name=primitive_name,
778 )
779 return "CONFIG OK"
780 else:
781 try:
782 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 if status == "completed":
792 return output
793 else:
794 raise Exception("status is not completed: {}".format(status))
795 except Exception as e:
796 self.log.error(
797 "Error executing primitive {}: {}".format(primitive_name, e)
798 )
799 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 async def disconnect(self):
807 self.log.info("closing juju N2VC...")
808 await self._juju_logout()
809
810 """
811 ####################################################################################
812 ################################### P R I V A T E ##################################
813 ####################################################################################
814 """
815
816 def _write_ee_id_db(self, db_dict: dict, ee_id: str):
817
818 # write ee_id to database: _admin.deployed.VCA.x
819 try:
820 the_table = db_dict["collection"]
821 the_filter = db_dict["filter"]
822 the_path = db_dict["path"]
823 if not the_path[-1] == ".":
824 the_path = the_path + "."
825 update_dict = {the_path + "ee_id": ee_id}
826 # self.log.debug('Writing ee_id to database: {}'.format(the_path))
827 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 except asyncio.CancelledError:
834 raise
835 except Exception as e:
836 self.log.error("Error writing ee_id to database: {}".format(e))
837
838 @staticmethod
839 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 return "{}.{}.{}".format(model_name, application_name, machine_id)
849
850 @staticmethod
851 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 if ee_id is None:
859 return None, None, None
860
861 # split components of id
862 parts = ee_id.split(".")
863 model_name = parts[0]
864 application_name = parts[1]
865 machine_id = parts[2]
866 return model_name, application_name, machine_id
867
868 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 _, _, vnf_id, vdu_id, vdu_count = self._get_namespace_components(
879 namespace=namespace
880 )
881
882 if vnf_id is None or len(vnf_id) == 0:
883 vnf_id = ""
884 else:
885 # Shorten the vnf_id to its last twelve characters
886 vnf_id = "vnf-" + vnf_id[-12:]
887
888 if vdu_id is None or len(vdu_id) == 0:
889 vdu_id = ""
890 else:
891 # Shorten the vdu_id to its last twelve characters
892 vdu_id = "-vdu-" + vdu_id[-12:]
893
894 if vdu_count is None or len(vdu_count) == 0:
895 vdu_count = ""
896 else:
897 vdu_count = "-cnt-" + vdu_count
898
899 application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
900
901 return N2VCJujuConnector._format_app_name(application_name)
902
903 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 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 model = await self._juju_get_model(model_name=model_name)
921 observer = self.juju_observers[model_name]
922
923 # find machine id in model
924 machine = None
925 if machine_id is not None:
926 self.log.debug("Finding existing machine id {} in model".format(machine_id))
927 # get juju existing machines in the model
928 existing_machines = await model.get_machines()
929 if machine_id in existing_machines:
930 self.log.debug(
931 "Machine id {} found in model (reusing it)".format(machine_id)
932 )
933 machine = model.machines[machine_id]
934
935 if machine is None:
936 self.log.debug("Creating a new machine in juju...")
937 # machine does not exist, create it and wait for it
938 machine = await model.add_machine(
939 spec=None, constraints=None, disks=None, series="xenial"
940 )
941
942 # register machine with observer
943 observer.register_machine(machine=machine, db_dict=db_dict)
944
945 # id for the execution environment
946 ee_id = N2VCJujuConnector._build_ee_id(
947 model_name=model_name,
948 application_name=application_name,
949 machine_id=str(machine.entity_id),
950 )
951
952 # write ee_id in database
953 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id)
954
955 # wait for machine creation
956 await observer.wait_for_machine(
957 machine_id=str(machine.entity_id),
958 progress_timeout=progress_timeout,
959 total_timeout=total_timeout,
960 )
961
962 else:
963
964 self.log.debug("Reusing old machine pending")
965
966 # register machine with observer
967 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 await observer.wait_for_machine(
972 machine_id=machine.entity_id,
973 progress_timeout=progress_timeout,
974 total_timeout=total_timeout,
975 )
976
977 self.log.debug("Machine ready at " + str(machine.dns_name))
978 return machine
979
980 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 if not self.api_proxy:
992 msg = "Cannot provision machine: api_proxy is not defined"
993 self.log.error(msg=msg)
994 raise N2VCException(message=msg)
995
996 self.log.debug(
997 "provisioning machine. model: {}, hostname: {}, username: {}".format(
998 model_name, hostname, username
999 )
1000 )
1001
1002 if not self._authenticated:
1003 await self._juju_login()
1004
1005 # get juju model and observer
1006 model = await self._juju_get_model(model_name=model_name)
1007 observer = self.juju_observers[model_name]
1008
1009 # TODO check if machine is already provisioned
1010 machine_list = await model.get_machines()
1011
1012 provisioner = AsyncSSHProvisioner(
1013 host=hostname,
1014 user=username,
1015 private_key_path=private_key_path,
1016 log=self.log,
1017 )
1018
1019 params = None
1020 try:
1021 params = await provisioner.provision_machine()
1022 except Exception as ex:
1023 msg = "Exception provisioning machine: {}".format(ex)
1024 self.log.error(msg)
1025 raise N2VCException(message=msg)
1026
1027 params.jobs = ["JobHostUnits"]
1028
1029 connection = model.connection()
1030
1031 # Submit the request.
1032 self.log.debug("Adding machine to model")
1033 client_facade = client.ClientFacade.from_connection(connection)
1034 results = await client_facade.AddMachines(params=[params])
1035 error = results.machines[0].error
1036 if error:
1037 msg = "Error adding machine: {}".format(error.message)
1038 self.log.error(msg=msg)
1039 raise ValueError(msg)
1040
1041 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 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
1046 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 machine = None
1058 for _ in range(10):
1059 machine_list = await model.get_machines()
1060 if machine_id in machine_list:
1061 self.log.debug("Machine {} found in model!".format(machine_id))
1062 machine = model.machines.get(machine_id)
1063 break
1064 await asyncio.sleep(2)
1065
1066 if machine is None:
1067 msg = "Machine {} not found in model".format(machine_id)
1068 self.log.error(msg=msg)
1069 raise Exception(msg)
1070
1071 # register machine with observer
1072 observer.register_machine(machine=machine, db_dict=db_dict)
1073
1074 # wait for machine creation
1075 self.log.debug("waiting for provision finishes... {}".format(machine_id))
1076 await observer.wait_for_machine(
1077 machine_id=machine_id,
1078 progress_timeout=progress_timeout,
1079 total_timeout=total_timeout,
1080 )
1081
1082 self.log.debug("Machine provisioned {}".format(machine_id))
1083
1084 return machine_id
1085
1086 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 model = await self._juju_get_model(model_name=model_name)
1100 observer = self.juju_observers[model_name]
1101
1102 # check if application already exists
1103 application = None
1104 if application_name in model.applications:
1105 application = model.applications[application_name]
1106
1107 if application is None:
1108
1109 # application does not exist, create it and wait for it
1110 self.log.debug(
1111 "deploying application {} to machine {}, model {}".format(
1112 application_name, machine_id, model_name
1113 )
1114 )
1115 self.log.debug("charm: {}".format(charm_path))
1116 machine = model.machines[machine_id]
1117 # series = None
1118 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 observer.register_application(application=application, db_dict=db_dict)
1130
1131 self.log.debug(
1132 "waiting for application deployed... {}".format(application.entity_id)
1133 )
1134 retries = await observer.wait_for_application(
1135 application_id=application.entity_id,
1136 progress_timeout=progress_timeout,
1137 total_timeout=total_timeout,
1138 )
1139 self.log.debug("application deployed")
1140
1141 else:
1142
1143 # register application with observer
1144 observer.register_application(application=application, db_dict=db_dict)
1145
1146 # application already exists, but not finalised
1147 self.log.debug("application already exists, waiting for deployed...")
1148 retries = await observer.wait_for_application(
1149 application_id=application.entity_id,
1150 progress_timeout=progress_timeout,
1151 total_timeout=total_timeout,
1152 )
1153 self.log.debug("application deployed")
1154
1155 return application, retries
1156
1157 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 model = await self._juju_get_model(model_name=model_name)
1170 observer = self.juju_observers[model_name]
1171
1172 application = await self._juju_get_application(
1173 model_name=model_name, application_name=application_name
1174 )
1175
1176 unit = None
1177 for u in application.units:
1178 if await u.is_leader_from_status():
1179 unit = u
1180 if unit is not None:
1181 actions = await application.get_actions()
1182 if action_name in actions:
1183 self.log.debug(
1184 'executing action "{}" using params: {}'.format(action_name, kwargs)
1185 )
1186 action = await unit.run_action(action_name, **kwargs)
1187
1188 # register action with observer
1189 observer.register_action(action=action, db_dict=db_dict)
1190
1191 await observer.wait_for_action(
1192 action_id=action.entity_id,
1193 progress_timeout=progress_timeout,
1194 total_timeout=total_timeout,
1195 )
1196 self.log.debug("action completed with status: {}".format(action.status))
1197 output = await model.get_action_output(action_uuid=action.entity_id)
1198 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1199 if action.entity_id in status:
1200 status = status[action.entity_id]
1201 else:
1202 status = "failed"
1203 return output, status
1204
1205 raise N2VCExecutionException(
1206 message="Cannot execute action on charm", primitive_name=action_name
1207 )
1208
1209 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 application = await self._juju_get_application(
1221 model_name=model_name, application_name=application_name
1222 )
1223
1224 self.log.debug(
1225 "configuring the application {} -> {}".format(application_name, config)
1226 )
1227 res = await application.set_config(config)
1228 self.log.debug(
1229 "application {} configured. res={}".format(application_name, res)
1230 )
1231
1232 # Verify the config is set
1233 new_conf = await application.get_config()
1234 for key in config:
1235 value = new_conf[key]["value"]
1236 self.log.debug(" {} = {}".format(key, value))
1237 if config[key] != value:
1238 raise N2VCException(
1239 message="key {} is not configured correctly {} != {}".format(
1240 key, config[key], new_conf[key]
1241 )
1242 )
1243
1244 # check if 'verify-ssh-credentials' action exists
1245 # unit = application.units[0]
1246 actions = await application.get_actions()
1247 if "verify-ssh-credentials" not in actions:
1248 msg = (
1249 "Action verify-ssh-credentials does not exist in application {}"
1250 ).format(application_name)
1251 self.log.debug(msg=msg)
1252 return False
1253
1254 # execute verify-credentials
1255 num_retries = 20
1256 retry_timeout = 15.0
1257 for _ in range(num_retries):
1258 try:
1259 self.log.debug("Executing action verify-ssh-credentials...")
1260 output, ok = await self._juju_execute_action(
1261 model_name=model_name,
1262 application_name=application_name,
1263 action_name="verify-ssh-credentials",
1264 db_dict=db_dict,
1265 progress_timeout=progress_timeout,
1266 total_timeout=total_timeout,
1267 )
1268 self.log.debug("Result: {}, output: {}".format(ok, output))
1269 return True
1270 except asyncio.CancelledError:
1271 raise
1272 except Exception as e:
1273 self.log.debug(
1274 "Error executing verify-ssh-credentials: {}. Retrying...".format(e)
1275 )
1276 await asyncio.sleep(retry_timeout)
1277 else:
1278 self.log.error(
1279 "Error executing verify-ssh-credentials after {} retries. ".format(
1280 num_retries
1281 )
1282 )
1283 return False
1284
1285 async def _juju_get_application(self, model_name: str, application_name: str):
1286 """Get the deployed application."""
1287
1288 model = await self._juju_get_model(model_name=model_name)
1289
1290 application_name = N2VCJujuConnector._format_app_name(application_name)
1291
1292 if model.applications and application_name in model.applications:
1293 return model.applications[application_name]
1294 else:
1295 raise N2VCException(
1296 message="Cannot get application {} from model {}".format(
1297 application_name, model_name
1298 )
1299 )
1300
1301 async def _juju_get_model(self, model_name: str) -> Model:
1302 """ Get a model object from juju controller
1303 If the model does not exits, it creates it.
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 model_name = N2VCJujuConnector._format_model_name(model_name)
1311
1312 if model_name in self.juju_models:
1313 return self.juju_models[model_name]
1314
1315 if self._creating_model:
1316 self.log.debug("Another coroutine is creating a model. Wait...")
1317 while self._creating_model:
1318 # another coroutine is creating a model, wait
1319 await asyncio.sleep(0.1)
1320 # retry (perhaps another coroutine has created the model meanwhile)
1321 if model_name in self.juju_models:
1322 return self.juju_models[model_name]
1323
1324 try:
1325 self._creating_model = True
1326
1327 # get juju model names from juju
1328 model_list = await self.controller.list_models()
1329
1330 if model_name not in model_list:
1331 self.log.info(
1332 "Model {} does not exist. Creating new model...".format(model_name)
1333 )
1334 config_dict = {"authorized-keys": self.public_key}
1335 if self.apt_mirror:
1336 config_dict["apt-mirror"] = self.apt_mirror
1337 if not self.enable_os_upgrade:
1338 config_dict["enable-os-refresh-update"] = False
1339 config_dict["enable-os-upgrade"] = False
1340 if self.cloud in self.BUILT_IN_CLOUDS:
1341 model = await self.controller.add_model(
1342 model_name=model_name,
1343 config=config_dict,
1344 cloud_name=self.cloud,
1345 )
1346 else:
1347 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 self.log.info("New model created, name={}".format(model_name))
1354 else:
1355 self.log.debug(
1356 "Model already exists in juju. Getting model {}".format(model_name)
1357 )
1358 model = await self.controller.get_model(model_name)
1359 self.log.debug("Existing model in juju, name={}".format(model_name))
1360
1361 self.juju_models[model_name] = model
1362 self.juju_observers[model_name] = JujuModelObserver(n2vc=self, model=model)
1363 return model
1364
1365 except Exception as e:
1366 msg = "Cannot get model {}. Exception: {}".format(model_name, e)
1367 self.log.error(msg)
1368 raise N2VCException(msg)
1369 finally:
1370 self._creating_model = False
1371
1372 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 model = await self._juju_get_model(model_name=model_name)
1383
1384 r1 = "{}:{}".format(application_name_1, relation_1)
1385 r2 = "{}:{}".format(application_name_2, relation_2)
1386
1387 self.log.debug("adding relation: {} -> {}".format(r1, r2))
1388 try:
1389 await model.add_relation(relation1=r1, relation2=r2)
1390 except JujuAPIError as e:
1391 # If one of the applications in the relationship doesn't exist, or the
1392 # relation has already been added,
1393 # let the operation fail silently.
1394 if "not found" in e.message:
1395 return
1396 if "already exists" in e.message:
1397 return
1398 # another execption, raise it
1399 raise e
1400
1401 async def _juju_destroy_application(self, model_name: str, application_name: str):
1402
1403 self.log.debug(
1404 "Destroying application {} in model {}".format(application_name, model_name)
1405 )
1406
1407 # get juju model and observer
1408 model = await self._juju_get_model(model_name=model_name)
1409 observer = self.juju_observers[model_name]
1410
1411 application = model.applications.get(application_name)
1412 if application:
1413 observer.unregister_application(application_name)
1414 await application.destroy()
1415 else:
1416 self.log.debug("Application not found: {}".format(application_name))
1417
1418 async def _juju_destroy_machine(
1419 self, model_name: str, machine_id: str, total_timeout: float = None
1420 ):
1421
1422 self.log.debug(
1423 "Destroying machine {} in model {}".format(machine_id, model_name)
1424 )
1425
1426 if total_timeout is None:
1427 total_timeout = 3600
1428
1429 # get juju model and observer
1430 model = await self._juju_get_model(model_name=model_name)
1431 observer = self.juju_observers[model_name]
1432
1433 machines = await model.get_machines()
1434 if machine_id in machines:
1435 machine = model.machines[machine_id]
1436 observer.unregister_machine(machine_id)
1437 # TODO: change this by machine.is_manual when this is upstreamed:
1438 # https://github.com/juju/python-libjuju/pull/396
1439 if "instance-id" in machine.safe_data and machine.safe_data[
1440 "instance-id"
1441 ].startswith("manual:"):
1442 self.log.debug("machine.destroy(force=True) started.")
1443 await machine.destroy(force=True)
1444 self.log.debug("machine.destroy(force=True) passed.")
1445 # max timeout
1446 end = time.time() + total_timeout
1447 # wait for machine removal
1448 machines = await model.get_machines()
1449 while machine_id in machines and time.time() < end:
1450 self.log.debug(
1451 "Waiting for machine {} is destroyed".format(machine_id)
1452 )
1453 await asyncio.sleep(0.5)
1454 machines = await model.get_machines()
1455 self.log.debug("Machine destroyed: {}".format(machine_id))
1456 else:
1457 self.log.debug("Machine not found: {}".format(machine_id))
1458
1459 async def _juju_destroy_model(self, model_name: str, total_timeout: float = None):
1460
1461 self.log.debug("Destroying model {}".format(model_name))
1462
1463 if total_timeout is None:
1464 total_timeout = 3600
1465 end = time.time() + total_timeout
1466
1467 model = await self._juju_get_model(model_name=model_name)
1468
1469 if not model:
1470 raise N2VCNotFound(message="Model {} does not exist".format(model_name))
1471
1472 uuid = model.info.uuid
1473
1474 # destroy applications
1475 for application_name in model.applications:
1476 try:
1477 await self._juju_destroy_application(
1478 model_name=model_name, application_name=application_name
1479 )
1480 except Exception as e:
1481 self.log.error(
1482 "Error destroying application {} in model {}: {}".format(
1483 application_name, model_name, e
1484 )
1485 )
1486
1487 # destroy machines
1488 machines = await model.get_machines()
1489 for machine_id in machines:
1490 try:
1491 await self._juju_destroy_machine(
1492 model_name=model_name, machine_id=machine_id
1493 )
1494 except asyncio.CancelledError:
1495 raise
1496 except Exception:
1497 # ignore exceptions destroying machine
1498 pass
1499
1500 await self._juju_disconnect_model(model_name=model_name)
1501
1502 self.log.debug("destroying model {}...".format(model_name))
1503 await self.controller.destroy_model(uuid)
1504 # self.log.debug('model destroy requested {}'.format(model_name))
1505
1506 # wait for model is completely destroyed
1507 self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
1508 last_exception = ""
1509 while time.time() < end:
1510 try:
1511 # await self.controller.get_model(uuid)
1512 models = await self.controller.list_models()
1513 if model_name not in models:
1514 self.log.debug(
1515 "The model {} ({}) was destroyed".format(model_name, uuid)
1516 )
1517 return
1518 except asyncio.CancelledError:
1519 raise
1520 except Exception as e:
1521 last_exception = e
1522 await asyncio.sleep(5)
1523 raise N2VCException(
1524 "Timeout waiting for model {} to be destroyed {}".format(
1525 model_name, last_exception
1526 )
1527 )
1528
1529 async def _juju_login(self):
1530 """Connect to juju controller
1531
1532 """
1533
1534 # if already authenticated, exit function
1535 if self._authenticated:
1536 return
1537
1538 # if connecting, wait for finish
1539 # another task could be trying to connect in parallel
1540 while self._connecting:
1541 await asyncio.sleep(0.1)
1542
1543 # double check after other task has finished
1544 if self._authenticated:
1545 return
1546
1547 try:
1548 self._connecting = True
1549 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 self.controller = Controller(loop=self.loop)
1560 # Connect to controller
1561 await self.controller.connect(
1562 endpoint=self.url,
1563 username=self.username,
1564 password=self.secret,
1565 cacert=self.ca_cert,
1566 )
1567 self._authenticated = True
1568 self.log.info("juju controller connected")
1569 except Exception as e:
1570 message = "Exception connecting to juju: {}".format(e)
1571 self.log.error(message)
1572 raise N2VCConnectionException(message=message, url=self.url)
1573 finally:
1574 self._connecting = False
1575
1576 async def _juju_logout(self):
1577 """Logout of the Juju controller."""
1578 if not self._authenticated:
1579 return False
1580
1581 # disconnect all models
1582 for model_name in self.juju_models:
1583 try:
1584 await self._juju_disconnect_model(model_name)
1585 except Exception as e:
1586 self.log.error(
1587 "Error disconnecting model {} : {}".format(model_name, e)
1588 )
1589 # continue with next model...
1590
1591 self.log.info("Disconnecting controller")
1592 try:
1593 await self.controller.disconnect()
1594 except Exception as e:
1595 raise N2VCConnectionException(
1596 message="Error disconnecting controller: {}".format(e), url=self.url
1597 )
1598
1599 self.controller = None
1600 self._authenticated = False
1601 self.log.info("disconnected")
1602
1603 async def _juju_disconnect_model(self, model_name: str):
1604 self.log.debug("Disconnecting model {}".format(model_name))
1605 if model_name in self.juju_models:
1606 await self.juju_models[model_name].disconnect()
1607 self.juju_models[model_name] = None
1608 self.juju_observers[model_name] = None
1609 else:
1610 self.warning("Cannot disconnect model: {}".format(model_name))
1611
1612 def _create_juju_public_key(self):
1613 """Recreate the Juju public key on lcm container, if needed
1614 Certain libjuju commands expect to be run from the same machine as Juju
1615 is bootstrapped to. This method will write the public key to disk in
1616 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
1617 """
1618
1619 # Make sure that we have a public key before writing to disk
1620 if self.public_key is None or len(self.public_key) == 0:
1621 if "OSMLCM_VCA_PUBKEY" in os.environ:
1622 self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
1623 if len(self.public_key) == 0:
1624 return
1625 else:
1626 return
1627
1628 pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
1629 file_path = "{}/juju_id_rsa.pub".format(pk_path)
1630 self.log.debug(
1631 "writing juju public key to file:\n{}\npublic key: {}".format(
1632 file_path, self.public_key
1633 )
1634 )
1635 if not os.path.exists(pk_path):
1636 # create path and write file
1637 os.makedirs(pk_path)
1638 with open(file_path, "w") as f:
1639 self.log.debug("Creating juju public key file: {}".format(file_path))
1640 f.write(self.public_key)
1641 else:
1642 self.log.debug("juju public key file already exists: {}".format(file_path))
1643
1644 @staticmethod
1645 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 return name.replace("_", "-").replace(" ", "-").lower()
1652
1653 @staticmethod
1654 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 def all_numbers(s: str) -> bool:
1667 for c in s:
1668 if not c.isdigit():
1669 return False
1670 return True
1671
1672 new_name = name.replace("_", "-")
1673 new_name = new_name.replace(" ", "-")
1674 new_name = new_name.lower()
1675 while new_name.find("--") >= 0:
1676 new_name = new_name.replace("--", "-")
1677 groups = new_name.split("-")
1678
1679 # find 'all numbers' groups and prefix them with a letter
1680 app_name = ""
1681 for i in range(len(groups)):
1682 group = groups[i]
1683 if all_numbers(group):
1684 group = "z" + group
1685 if i > 0:
1686 app_name += "-"
1687 app_name += group
1688
1689 if app_name[0].isdigit():
1690 app_name = "z" + app_name
1691
1692 return app_name