Destroy machines before destroying the model
[osm/N2VC.git] / n2vc / k8s_juju_conn.py
1 # Copyright 2019 Canonical Ltd.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import concurrent
16 from .exceptions import NotImplemented
17
18 import juju
19 # from juju.bundle import BundleHandler
20 from juju.controller import Controller
21 from juju.model import Model
22 from juju.errors import JujuAPIError, JujuError
23
24 import logging
25
26 from n2vc.k8s_conn import K8sConnector
27
28 import os
29 # import re
30 # import ssl
31 import subprocess
32 # from .vnf import N2VC
33
34 import uuid
35 import yaml
36
37
38 class K8sJujuConnector(K8sConnector):
39
40 def __init__(
41 self,
42 fs: object,
43 db: object,
44 kubectl_command: str = '/usr/bin/kubectl',
45 juju_command: str = '/usr/bin/juju',
46 log=None,
47 on_update_db=None,
48 ):
49 """
50
51 :param kubectl_command: path to kubectl executable
52 :param helm_command: path to helm executable
53 :param fs: file system for kubernetes and helm configuration
54 :param log: logger
55 """
56
57 # parent class
58 K8sConnector.__init__(
59 self,
60 db,
61 log=log,
62 on_update_db=on_update_db,
63 )
64
65 self.fs = fs
66 self.info('Initializing K8S Juju connector')
67
68 self.authenticated = False
69 self.models = {}
70 self.log = logging.getLogger(__name__)
71
72 self.juju_command = juju_command
73 self.juju_secret = ""
74
75 self.info('K8S Juju connector initialized')
76
77 """Initialization"""
78 async def init_env(
79 self,
80 k8s_creds: str,
81 namespace: str = 'kube-system',
82 reuse_cluster_uuid: str = None,
83 ) -> (str, bool):
84 """Initialize a Kubernetes environment
85
86 :param k8s_creds dict: A dictionary containing the Kubernetes cluster
87 configuration
88 :param namespace str: The Kubernetes namespace to initialize
89
90 :return: UUID of the k8s context or raises an exception
91 """
92
93 """Bootstrapping
94
95 Bootstrapping cannot be done, by design, through the API. We need to
96 use the CLI tools.
97 """
98
99 """
100 WIP: Workflow
101
102 1. Has the environment already been bootstrapped?
103 - Check the database to see if we have a record for this env
104
105 2. If this is a new env, create it
106 - Add the k8s cloud to Juju
107 - Bootstrap
108 - Record it in the database
109
110 3. Connect to the Juju controller for this cloud
111
112 """
113 # cluster_uuid = reuse_cluster_uuid
114 # if not cluster_uuid:
115 # cluster_uuid = str(uuid4())
116
117 ##################################################
118 # TODO: Pull info from db based on the namespace #
119 ##################################################
120
121 if not reuse_cluster_uuid:
122 # This is a new cluster, so bootstrap it
123
124 cluster_uuid = str(uuid.uuid4())
125
126 # Is a local k8s cluster?
127 localk8s = self.is_local_k8s(k8s_creds)
128
129 # If the k8s is external, the juju controller needs a loadbalancer
130 loadbalancer = False if localk8s else True
131
132 # Name the new k8s cloud
133 k8s_cloud = "{}-k8s".format(namespace)
134
135 print("Adding k8s cloud {}".format(k8s_cloud))
136 await self.add_k8s(k8s_cloud, k8s_creds)
137
138 # Bootstrap Juju controller
139 print("Bootstrapping...")
140 await self.bootstrap(k8s_cloud, cluster_uuid, loadbalancer)
141 print("Bootstrap done.")
142
143 # Get the controller information
144
145 # Parse ~/.local/share/juju/controllers.yaml
146 # controllers.testing.api-endpoints|ca-cert|uuid
147 print("Getting controller endpoints")
148 with open(os.path.expanduser(
149 "~/.local/share/juju/controllers.yaml"
150 )) as f:
151 controllers = yaml.load(f, Loader=yaml.Loader)
152 controller = controllers['controllers'][cluster_uuid]
153 endpoints = controller['api-endpoints']
154 self.juju_endpoint = endpoints[0]
155 self.juju_ca_cert = controller['ca-cert']
156
157 # Parse ~/.local/share/juju/accounts
158 # controllers.testing.user|password
159 print("Getting accounts")
160 with open(os.path.expanduser(
161 "~/.local/share/juju/accounts.yaml"
162 )) as f:
163 controllers = yaml.load(f, Loader=yaml.Loader)
164 controller = controllers['controllers'][cluster_uuid]
165
166 self.juju_user = controller['user']
167 self.juju_secret = controller['password']
168
169 print("user: {}".format(self.juju_user))
170 print("secret: {}".format(self.juju_secret))
171 print("endpoint: {}".format(self.juju_endpoint))
172 print("ca-cert: {}".format(self.juju_ca_cert))
173
174 # raise Exception("EOL")
175
176 self.juju_public_key = None
177
178 config = {
179 'endpoint': self.juju_endpoint,
180 'username': self.juju_user,
181 'secret': self.juju_secret,
182 'cacert': self.juju_ca_cert,
183 'namespace': namespace,
184 'loadbalancer': loadbalancer,
185 }
186
187 # Store the cluster configuration so it
188 # can be used for subsequent calls
189 print("Setting config")
190 await self.set_config(cluster_uuid, config)
191
192 else:
193 # This is an existing cluster, so get its config
194 cluster_uuid = reuse_cluster_uuid
195
196 config = self.get_config(cluster_uuid)
197
198 self.juju_endpoint = config['endpoint']
199 self.juju_user = config['username']
200 self.juju_secret = config['secret']
201 self.juju_ca_cert = config['cacert']
202 self.juju_public_key = None
203
204 # Login to the k8s cluster
205 if not self.authenticated:
206 await self.login(cluster_uuid)
207
208 # We're creating a new cluster
209 print("Getting model {}".format(self.get_namespace(cluster_uuid), cluster_uuid=cluster_uuid))
210 model = await self.get_model(
211 self.get_namespace(cluster_uuid),
212 cluster_uuid=cluster_uuid
213 )
214
215 # Disconnect from the model
216 if model and model.is_connected():
217 await model.disconnect()
218
219 return cluster_uuid, True
220
221 """Repo Management"""
222 async def repo_add(
223 self,
224 name: str,
225 url: str,
226 type: str = "charm",
227 ):
228 raise NotImplemented()
229
230 async def repo_list(self):
231 raise NotImplemented()
232
233 async def repo_remove(
234 self,
235 name: str,
236 ):
237 raise NotImplemented()
238
239 """Reset"""
240 async def reset(
241 self,
242 cluster_uuid: str,
243 force: bool = False,
244 uninstall_sw: bool = False
245 ) -> bool:
246 """Reset a cluster
247
248 Resets the Kubernetes cluster by removing the model that represents it.
249
250 :param cluster_uuid str: The UUID of the cluster to reset
251 :return: Returns True if successful or raises an exception.
252 """
253
254 try:
255 if not self.authenticated:
256 await self.login(cluster_uuid)
257
258 if self.controller.is_connected():
259 # Destroy the model
260 namespace = self.get_namespace(cluster_uuid)
261 if await self.has_model(namespace):
262 print("[reset] Destroying model")
263 await self.controller.destroy_model(
264 namespace,
265 destroy_storage=True
266 )
267
268 # Disconnect from the controller
269 print("[reset] Disconnecting controller")
270 await self.controller.disconnect()
271
272 # Destroy the controller (via CLI)
273 print("[reset] Destroying controller")
274 await self.destroy_controller(cluster_uuid)
275
276 print("[reset] Removing k8s cloud")
277 namespace = self.get_namespace(cluster_uuid)
278 k8s_cloud = "{}-k8s".format(namespace)
279 await self.remove_cloud(k8s_cloud)
280
281 except Exception as ex:
282 print("Caught exception during reset: {}".format(ex))
283
284 """Deployment"""
285
286 async def install(
287 self,
288 cluster_uuid: str,
289 kdu_model: str,
290 atomic: bool = True,
291 timeout: float = 300,
292 params: dict = None,
293 db_dict: dict = None
294 ) -> bool:
295 """Install a bundle
296
297 :param cluster_uuid str: The UUID of the cluster to install to
298 :param kdu_model str: The name or path of a bundle to install
299 :param atomic bool: If set, waits until the model is active and resets
300 the cluster on failure.
301 :param timeout int: The time, in seconds, to wait for the install
302 to finish
303 :param params dict: Key-value pairs of instantiation parameters
304
305 :return: If successful, returns ?
306 """
307
308 if not self.authenticated:
309 print("[install] Logging in to the controller")
310 await self.login(cluster_uuid)
311
312 ##
313 # Get or create the model, based on the NS
314 # uuid.
315 model_name = db_dict["filter"]["_id"]
316
317 self.log.debug("Checking for model named {}".format(model_name))
318 model = await self.get_model(model_name, cluster_uuid=cluster_uuid)
319 if not model:
320 # Create the new model
321 self.log.debug("Adding model: {}".format(model_name))
322 model = await self.add_model(model_name, cluster_uuid=cluster_uuid)
323
324 if model:
325 # TODO: Instantiation parameters
326
327 """
328 "Juju bundle that models the KDU, in any of the following ways:
329 - <juju-repo>/<juju-bundle>
330 - <juju-bundle folder under k8s_models folder in the package>
331 - <juju-bundle tgz file (w/ or w/o extension) under k8s_models folder in the package>
332 - <URL_where_to_fetch_juju_bundle>
333 """
334
335 bundle = kdu_model
336 if kdu_model.startswith("cs:"):
337 bundle = kdu_model
338 elif kdu_model.startswith("http"):
339 # Download the file
340 pass
341 else:
342 # Local file
343
344 # if kdu_model.endswith(".tar.gz") or kdu_model.endswith(".tgz")
345 # Uncompress temporarily
346 # bundle = <uncompressed file>
347 pass
348
349 if not bundle:
350 # Raise named exception that the bundle could not be found
351 raise Exception()
352
353 print("[install] deploying {}".format(bundle))
354 await model.deploy(bundle)
355
356 # Get the application
357 if atomic:
358 # applications = model.applications
359 print("[install] Applications: {}".format(model.applications))
360 for name in model.applications:
361 print("[install] Waiting for {} to settle".format(name))
362 application = model.applications[name]
363 try:
364 # It's not enough to wait for all units to be active;
365 # the application status needs to be active as well.
366 print("Waiting for all units to be active...")
367 await model.block_until(
368 lambda: all(
369 unit.agent_status == 'idle'
370 and application.status in ['active', 'unknown']
371 and unit.workload_status in [
372 'active', 'unknown'
373 ] for unit in application.units
374 ),
375 timeout=timeout
376 )
377 print("All units active.")
378
379 except concurrent.futures._base.TimeoutError:
380 print("[install] Timeout exceeded; resetting cluster")
381 await self.reset(cluster_uuid)
382 return False
383
384 # Wait for the application to be active
385 if model.is_connected():
386 print("[install] Disconnecting model")
387 await model.disconnect()
388
389 return True
390 raise Exception("Unable to install")
391
392 async def instances_list(
393 self,
394 cluster_uuid: str
395 ) -> list:
396 """
397 returns a list of deployed releases in a cluster
398
399 :param cluster_uuid: the cluster
400 :return:
401 """
402 return []
403
404 async def upgrade(
405 self,
406 cluster_uuid: str,
407 kdu_instance: str,
408 kdu_model: str = None,
409 params: dict = None,
410 ) -> str:
411 """Upgrade a model
412
413 :param cluster_uuid str: The UUID of the cluster to upgrade
414 :param kdu_instance str: The unique name of the KDU instance
415 :param kdu_model str: The name or path of the bundle to upgrade to
416 :param params dict: Key-value pairs of instantiation parameters
417
418 :return: If successful, reference to the new revision number of the
419 KDU instance.
420 """
421
422 # TODO: Loop through the bundle and upgrade each charm individually
423
424 """
425 The API doesn't have a concept of bundle upgrades, because there are
426 many possible changes: charm revision, disk, number of units, etc.
427
428 As such, we are only supporting a limited subset of upgrades. We'll
429 upgrade the charm revision but leave storage and scale untouched.
430
431 Scale changes should happen through OSM constructs, and changes to
432 storage would require a redeployment of the service, at least in this
433 initial release.
434 """
435 namespace = self.get_namespace(cluster_uuid)
436 model = await self.get_model(namespace, cluster_uuid=cluster_uuid)
437
438 with open(kdu_model, 'r') as f:
439 bundle = yaml.safe_load(f)
440
441 """
442 {
443 'description': 'Test bundle',
444 'bundle': 'kubernetes',
445 'applications': {
446 'mariadb-k8s': {
447 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
448 'scale': 1,
449 'options': {
450 'password': 'manopw',
451 'root_password': 'osm4u',
452 'user': 'mano'
453 },
454 'series': 'kubernetes'
455 }
456 }
457 }
458 """
459 # TODO: This should be returned in an agreed-upon format
460 for name in bundle['applications']:
461 print(model.applications)
462 application = model.applications[name]
463 print(application)
464
465 path = bundle['applications'][name]['charm']
466
467 try:
468 await application.upgrade_charm(switch=path)
469 except juju.errors.JujuError as ex:
470 if 'already running charm' in str(ex):
471 # We're already running this version
472 pass
473
474 await model.disconnect()
475
476 return True
477 raise NotImplemented()
478
479 """Rollback"""
480 async def rollback(
481 self,
482 cluster_uuid: str,
483 kdu_instance: str,
484 revision: int = 0,
485 ) -> str:
486 """Rollback a model
487
488 :param cluster_uuid str: The UUID of the cluster to rollback
489 :param kdu_instance str: The unique name of the KDU instance
490 :param revision int: The revision to revert to. If omitted, rolls back
491 the previous upgrade.
492
493 :return: If successful, returns the revision of active KDU instance,
494 or raises an exception
495 """
496 raise NotImplemented()
497
498 """Deletion"""
499 async def uninstall(
500 self,
501 cluster_uuid: str,
502 kdu_instance: str,
503 ) -> bool:
504 """Uninstall a KDU instance
505
506 :param cluster_uuid str: The UUID of the cluster to uninstall
507 :param kdu_instance str: The unique name of the KDU instance
508
509 :return: Returns True if successful, or raises an exception
510 """
511 removed = False
512
513 # Remove an application from the model
514 model = await self.get_model(self.get_namespace(cluster_uuid), cluster_uuid=cluster_uuid)
515
516 if model:
517 # Get the application
518 if kdu_instance not in model.applications:
519 # TODO: Raise a named exception
520 raise Exception("Application not found.")
521
522 application = model.applications[kdu_instance]
523
524 # Destroy the application
525 await application.destroy()
526
527 # TODO: Verify removal
528
529 removed = True
530 return removed
531
532 """Introspection"""
533 async def inspect_kdu(
534 self,
535 kdu_model: str,
536 ) -> dict:
537 """Inspect a KDU
538
539 Inspects a bundle and returns a dictionary of config parameters and
540 their default values.
541
542 :param kdu_model str: The name or path of the bundle to inspect.
543
544 :return: If successful, returns a dictionary of available parameters
545 and their default values.
546 """
547
548 kdu = {}
549 with open(kdu_model, 'r') as f:
550 bundle = yaml.safe_load(f)
551
552 """
553 {
554 'description': 'Test bundle',
555 'bundle': 'kubernetes',
556 'applications': {
557 'mariadb-k8s': {
558 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
559 'scale': 1,
560 'options': {
561 'password': 'manopw',
562 'root_password': 'osm4u',
563 'user': 'mano'
564 },
565 'series': 'kubernetes'
566 }
567 }
568 }
569 """
570 # TODO: This should be returned in an agreed-upon format
571 kdu = bundle['applications']
572
573 return kdu
574
575 async def help_kdu(
576 self,
577 kdu_model: str,
578 ) -> str:
579 """View the README
580
581 If available, returns the README of the bundle.
582
583 :param kdu_model str: The name or path of a bundle
584
585 :return: If found, returns the contents of the README.
586 """
587 readme = None
588
589 files = ['README', 'README.txt', 'README.md']
590 path = os.path.dirname(kdu_model)
591 for file in os.listdir(path):
592 if file in files:
593 with open(file, 'r') as f:
594 readme = f.read()
595 break
596
597 return readme
598
599 async def status_kdu(
600 self,
601 cluster_uuid: str,
602 kdu_instance: str,
603 ) -> dict:
604 """Get the status of the KDU
605
606 Get the current status of the KDU instance.
607
608 :param cluster_uuid str: The UUID of the cluster
609 :param kdu_instance str: The unique id of the KDU instance
610
611 :return: Returns a dictionary containing namespace, state, resources,
612 and deployment_time.
613 """
614 status = {}
615
616 model = await self.get_model(self.get_namespace(cluster_uuid), cluster_uuid=cluster_uuid)
617
618 # model = await self.get_model_by_uuid(cluster_uuid)
619 if model:
620 model_status = await model.get_status()
621 status = model_status.applications
622
623 for name in model_status.applications:
624 application = model_status.applications[name]
625 status[name] = {
626 'status': application['status']['status']
627 }
628
629 if model.is_connected():
630 await model.disconnect()
631
632 return status
633
634 # Private methods
635 async def add_k8s(
636 self,
637 cloud_name: str,
638 credentials: str,
639 ) -> bool:
640 """Add a k8s cloud to Juju
641
642 Adds a Kubernetes cloud to Juju, so it can be bootstrapped with a
643 Juju Controller.
644
645 :param cloud_name str: The name of the cloud to add.
646 :param credentials dict: A dictionary representing the output of
647 `kubectl config view --raw`.
648
649 :returns: True if successful, otherwise raises an exception.
650 """
651
652 cmd = [self.juju_command, "add-k8s", "--local", cloud_name]
653 print(cmd)
654 p = subprocess.run(
655 cmd,
656 stdout=subprocess.PIPE,
657 stderr=subprocess.PIPE,
658 # input=yaml.dump(credentials, Dumper=yaml.Dumper).encode("utf-8"),
659 input=credentials.encode("utf-8"),
660 # encoding='ascii'
661 )
662 retcode = p.returncode
663 print("add-k8s return code: {}".format(retcode))
664
665 if retcode > 0:
666 raise Exception(p.stderr)
667 return True
668
669 async def add_model(
670 self,
671 model_name: str,
672 cluster_uuid: str,
673 ) -> juju.model.Model:
674 """Adds a model to the controller
675
676 Adds a new model to the Juju controller
677
678 :param model_name str: The name of the model to add.
679 :returns: The juju.model.Model object of the new model upon success or
680 raises an exception.
681 """
682 if not self.authenticated:
683 await self.login(cluster_uuid)
684
685 self.log.debug("Adding model '{}' to cluster_uuid '{}'".format(model_name, cluster_uuid))
686 model = await self.controller.add_model(
687 model_name,
688 config={'authorized-keys': self.juju_public_key}
689 )
690 return model
691
692 async def bootstrap(
693 self,
694 cloud_name: str,
695 cluster_uuid: str,
696 loadbalancer: bool
697 ) -> bool:
698 """Bootstrap a Kubernetes controller
699
700 Bootstrap a Juju controller inside the Kubernetes cluster
701
702 :param cloud_name str: The name of the cloud.
703 :param cluster_uuid str: The UUID of the cluster to bootstrap.
704 :param loadbalancer bool: If the controller should use loadbalancer or not.
705 :returns: True upon success or raises an exception.
706 """
707
708 if not loadbalancer:
709 cmd = [self.juju_command, "bootstrap", cloud_name, cluster_uuid]
710 else:
711 """
712 For public clusters, specify that the controller service is using a LoadBalancer.
713 """
714 cmd = [self.juju_command, "bootstrap", cloud_name, cluster_uuid, "--config", "controller-service-type=loadbalancer"]
715
716 print("Bootstrapping controller {} in cloud {}".format(
717 cluster_uuid, cloud_name
718 ))
719
720 p = subprocess.run(
721 cmd,
722 stdout=subprocess.PIPE,
723 stderr=subprocess.PIPE,
724 # encoding='ascii'
725 )
726 retcode = p.returncode
727
728 if retcode > 0:
729 #
730 if b'already exists' not in p.stderr:
731 raise Exception(p.stderr)
732
733 return True
734
735 async def destroy_controller(
736 self,
737 cluster_uuid: str
738 ) -> bool:
739 """Destroy a Kubernetes controller
740
741 Destroy an existing Kubernetes controller.
742
743 :param cluster_uuid str: The UUID of the cluster to bootstrap.
744 :returns: True upon success or raises an exception.
745 """
746 cmd = [
747 self.juju_command,
748 "destroy-controller",
749 "--destroy-all-models",
750 "--destroy-storage",
751 "-y",
752 cluster_uuid
753 ]
754
755 p = subprocess.run(
756 cmd,
757 stdout=subprocess.PIPE,
758 stderr=subprocess.PIPE,
759 # encoding='ascii'
760 )
761 retcode = p.returncode
762
763 if retcode > 0:
764 #
765 if 'already exists' not in p.stderr:
766 raise Exception(p.stderr)
767
768 def get_config(
769 self,
770 cluster_uuid: str,
771 ) -> dict:
772 """Get the cluster configuration
773
774 Gets the configuration of the cluster
775
776 :param cluster_uuid str: The UUID of the cluster.
777 :return: A dict upon success, or raises an exception.
778 """
779 cluster_config = "{}/{}.yaml".format(self.fs.path, cluster_uuid)
780 if os.path.exists(cluster_config):
781 with open(cluster_config, 'r') as f:
782 config = yaml.safe_load(f.read())
783 return config
784 else:
785 raise Exception(
786 "Unable to locate configuration for cluster {}".format(
787 cluster_uuid
788 )
789 )
790
791 async def get_model(
792 self,
793 model_name: str,
794 cluster_uuid: str,
795 ) -> juju.model.Model:
796 """Get a model from the Juju Controller.
797
798 Note: Model objects returned must call disconnected() before it goes
799 out of scope.
800
801 :param model_name str: The name of the model to get
802 :return The juju.model.Model object if found, or None.
803 """
804 if not self.authenticated:
805 await self.login(cluster_uuid)
806
807 model = None
808 models = await self.controller.list_models()
809 self.log.debug(models)
810 if model_name in models:
811 self.log.debug("Found model: {}".format(model_name))
812 model = await self.controller.get_model(
813 model_name
814 )
815 return model
816
817 def get_namespace(
818 self,
819 cluster_uuid: str,
820 ) -> str:
821 """Get the namespace UUID
822 Gets the namespace's unique name
823
824 :param cluster_uuid str: The UUID of the cluster
825 :returns: The namespace UUID, or raises an exception
826 """
827 config = self.get_config(cluster_uuid)
828
829 # Make sure the name is in the config
830 if 'namespace' not in config:
831 raise Exception("Namespace not found.")
832
833 # TODO: We want to make sure this is unique to the cluster, in case
834 # the cluster is being reused.
835 # Consider pre/appending the cluster id to the namespace string
836 return config['namespace']
837
838 async def has_model(
839 self,
840 model_name: str
841 ) -> bool:
842 """Check if a model exists in the controller
843
844 Checks to see if a model exists in the connected Juju controller.
845
846 :param model_name str: The name of the model
847 :return: A boolean indicating if the model exists
848 """
849 models = await self.controller.list_models()
850
851 if model_name in models:
852 return True
853 return False
854
855 def is_local_k8s(
856 self,
857 credentials: str,
858 ) -> bool:
859 """Check if a cluster is local
860
861 Checks if a cluster is running in the local host
862
863 :param credentials dict: A dictionary containing the k8s credentials
864 :returns: A boolean if the cluster is running locally
865 """
866 creds = yaml.safe_load(credentials)
867 if os.getenv("OSMLCM_VCA_APIPROXY"):
868 host_ip = os.getenv("OSMLCM_VCA_APIPROXY")
869
870 if creds and host_ip:
871 for cluster in creds['clusters']:
872 if 'server' in cluster['cluster']:
873 if host_ip in cluster['cluster']['server']:
874 return True
875
876 return False
877
878 async def login(self, cluster_uuid):
879 """Login to the Juju controller."""
880
881 if self.authenticated:
882 return
883
884 self.connecting = True
885
886 # Test: Make sure we have the credentials loaded
887 config = self.get_config(cluster_uuid)
888
889 self.juju_endpoint = config['endpoint']
890 self.juju_user = config['username']
891 self.juju_secret = config['secret']
892 self.juju_ca_cert = config['cacert']
893 self.juju_public_key = None
894
895 self.controller = Controller()
896
897 if self.juju_secret:
898 self.log.debug(
899 "Connecting to controller... ws://{} as {}/{}".format(
900 self.juju_endpoint,
901 self.juju_user,
902 self.juju_secret,
903 )
904 )
905 try:
906 await self.controller.connect(
907 endpoint=self.juju_endpoint,
908 username=self.juju_user,
909 password=self.juju_secret,
910 cacert=self.juju_ca_cert,
911 )
912 self.authenticated = True
913 self.log.debug("JujuApi: Logged into controller")
914 except Exception as ex:
915 print(ex)
916 self.log.debug("Caught exception: {}".format(ex))
917 pass
918 else:
919 self.log.fatal("VCA credentials not configured.")
920 self.authenticated = False
921
922 async def logout(self):
923 """Logout of the Juju controller."""
924 print("[logout]")
925 if not self.authenticated:
926 return False
927
928 for model in self.models:
929 print("Logging out of model {}".format(model))
930 await self.models[model].disconnect()
931
932 if self.controller:
933 self.log.debug("Disconnecting controller {}".format(
934 self.controller
935 ))
936 await self.controller.disconnect()
937 self.controller = None
938
939 self.authenticated = False
940
941 async def remove_cloud(
942 self,
943 cloud_name: str,
944 ) -> bool:
945 """Remove a k8s cloud from Juju
946
947 Removes a Kubernetes cloud from Juju.
948
949 :param cloud_name str: The name of the cloud to add.
950
951 :returns: True if successful, otherwise raises an exception.
952 """
953
954 # Remove the bootstrapped controller
955 cmd = [self.juju_command, "remove-k8s", "--client", cloud_name]
956 p = subprocess.run(
957 cmd,
958 stdout=subprocess.PIPE,
959 stderr=subprocess.PIPE,
960 # encoding='ascii'
961 )
962 retcode = p.returncode
963
964 if retcode > 0:
965 raise Exception(p.stderr)
966
967 # Remove the cloud from the local config
968 cmd = [self.juju_command, "remove-cloud", "--client", cloud_name]
969 p = subprocess.run(
970 cmd,
971 stdout=subprocess.PIPE,
972 stderr=subprocess.PIPE,
973 # encoding='ascii'
974 )
975 retcode = p.returncode
976
977 if retcode > 0:
978 raise Exception(p.stderr)
979
980 return True
981
982 async def set_config(
983 self,
984 cluster_uuid: str,
985 config: dict,
986 ) -> bool:
987 """Save the cluster configuration
988
989 Saves the cluster information to the file store
990
991 :param cluster_uuid str: The UUID of the cluster
992 :param config dict: A dictionary containing the cluster configuration
993 :returns: Boolean upon success or raises an exception.
994 """
995
996 cluster_config = "{}/{}.yaml".format(self.fs.path, cluster_uuid)
997 if not os.path.exists(cluster_config):
998 print("Writing config to {}".format(cluster_config))
999 with open(cluster_config, 'w') as f:
1000 f.write(yaml.dump(config, Dumper=yaml.Dumper))
1001
1002 return True