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