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