1 # Copyright 2019 Canonical Ltd.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 from .exceptions
import NotImplemented
21 # from juju.bundle import BundleHandler
22 from juju
.controller
import Controller
23 from juju
.model
import Model
24 from juju
.errors
import JujuAPIError
, JujuError
25 from n2vc
.exceptions
import K8sException
27 from n2vc
.k8s_conn
import K8sConnector
32 # from .vnf import N2VC
38 class K8sJujuConnector(K8sConnector
):
44 kubectl_command
: str = '/usr/bin/kubectl',
45 juju_command
: str = '/usr/bin/juju',
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
58 K8sConnector
.__init
__(
62 on_update_db
=on_update_db
,
66 self
.log
.debug('Initializing K8S Juju connector')
68 self
.authenticated
= False
71 self
.juju_command
= juju_command
74 self
.log
.debug('K8S Juju connector initialized')
80 namespace
: str = 'kube-system',
81 reuse_cluster_uuid
: str = None,
84 It prepares a given K8s cluster environment to run Juju bundles.
86 :param k8s_creds: credentials to access a given K8s cluster, i.e. a valid '.kube/config'
87 :param namespace: optional namespace to be used for juju. By default, 'kube-system' will be used
88 :param reuse_cluster_uuid: existing cluster uuid for reuse
89 :return: uuid of the K8s cluster and True if connector has installed some software in the cluster
90 (on error, an exception will be raised)
95 Bootstrapping cannot be done, by design, through the API. We need to
102 1. Has the environment already been bootstrapped?
103 - Check the database to see if we have a record for this env
105 2. If this is a new env, create it
106 - Add the k8s cloud to Juju
108 - Record it in the database
110 3. Connect to the Juju controller for this cloud
113 # cluster_uuid = reuse_cluster_uuid
114 # if not cluster_uuid:
115 # cluster_uuid = str(uuid4())
117 ##################################################
118 # TODO: Pull info from db based on the namespace #
119 ##################################################
121 ###################################################
122 # TODO: Make it idempotent, calling add-k8s and #
123 # bootstrap whenever reuse_cluster_uuid is passed #
125 # `init_env` is called to initialize the K8s #
126 # cluster for juju. If this initialization fails, #
127 # it can be called again by LCM with the param #
128 # reuse_cluster_uuid, e.g. to try to fix it. #
129 ###################################################
131 if not reuse_cluster_uuid
:
132 # This is a new cluster, so bootstrap it
134 cluster_uuid
= str(uuid
.uuid4())
136 # Is a local k8s cluster?
137 localk8s
= self
.is_local_k8s(k8s_creds
)
139 # If the k8s is external, the juju controller needs a loadbalancer
140 loadbalancer
= False if localk8s
else True
142 # Name the new k8s cloud
143 k8s_cloud
= "k8s-{}".format(cluster_uuid
)
145 self
.log
.debug("Adding k8s cloud {}".format(k8s_cloud
))
146 await self
.add_k8s(k8s_cloud
, k8s_creds
)
148 # Bootstrap Juju controller
149 self
.log
.debug("Bootstrapping...")
150 await self
.bootstrap(k8s_cloud
, cluster_uuid
, loadbalancer
)
151 self
.log
.debug("Bootstrap done.")
153 # Get the controller information
155 # Parse ~/.local/share/juju/controllers.yaml
156 # controllers.testing.api-endpoints|ca-cert|uuid
157 self
.log
.debug("Getting controller endpoints")
158 with
open(os
.path
.expanduser(
159 "~/.local/share/juju/controllers.yaml"
161 controllers
= yaml
.load(f
, Loader
=yaml
.Loader
)
162 controller
= controllers
['controllers'][cluster_uuid
]
163 endpoints
= controller
['api-endpoints']
164 self
.juju_endpoint
= endpoints
[0]
165 self
.juju_ca_cert
= controller
['ca-cert']
167 # Parse ~/.local/share/juju/accounts
168 # controllers.testing.user|password
169 self
.log
.debug("Getting accounts")
170 with
open(os
.path
.expanduser(
171 "~/.local/share/juju/accounts.yaml"
173 controllers
= yaml
.load(f
, Loader
=yaml
.Loader
)
174 controller
= controllers
['controllers'][cluster_uuid
]
176 self
.juju_user
= controller
['user']
177 self
.juju_secret
= controller
['password']
179 # raise Exception("EOL")
181 self
.juju_public_key
= None
184 'endpoint': self
.juju_endpoint
,
185 'username': self
.juju_user
,
186 'secret': self
.juju_secret
,
187 'cacert': self
.juju_ca_cert
,
188 'namespace': namespace
,
189 'loadbalancer': loadbalancer
,
192 # Store the cluster configuration so it
193 # can be used for subsequent calls
194 self
.log
.debug("Setting config")
195 await self
.set_config(cluster_uuid
, config
)
198 # This is an existing cluster, so get its config
199 cluster_uuid
= reuse_cluster_uuid
201 config
= self
.get_config(cluster_uuid
)
203 self
.juju_endpoint
= config
['endpoint']
204 self
.juju_user
= config
['username']
205 self
.juju_secret
= config
['secret']
206 self
.juju_ca_cert
= config
['cacert']
207 self
.juju_public_key
= None
209 # Login to the k8s cluster
210 if not self
.authenticated
:
211 await self
.login(cluster_uuid
)
213 # We're creating a new cluster
214 #print("Getting model {}".format(self.get_namespace(cluster_uuid), cluster_uuid=cluster_uuid))
215 #model = await self.get_model(
216 # self.get_namespace(cluster_uuid),
217 # cluster_uuid=cluster_uuid
220 ## Disconnect from the model
221 #if model and model.is_connected():
222 # await model.disconnect()
224 return cluster_uuid
, True
226 """Repo Management"""
233 raise NotImplemented()
235 async def repo_list(self
):
236 raise NotImplemented()
238 async def repo_remove(
242 raise NotImplemented()
244 async def synchronize_repos(
250 Returns None as currently add_repo is not implemented
259 uninstall_sw
: bool = False
263 Resets the Kubernetes cluster by removing the model that represents it.
265 :param cluster_uuid str: The UUID of the cluster to reset
266 :return: Returns True if successful or raises an exception.
270 if not self
.authenticated
:
271 await self
.login(cluster_uuid
)
273 if self
.controller
.is_connected():
275 namespace
= self
.get_namespace(cluster_uuid
)
276 if await self
.has_model(namespace
):
277 self
.log
.debug("[reset] Destroying model")
278 await self
.controller
.destroy_model(
283 # Disconnect from the controller
284 self
.log
.debug("[reset] Disconnecting controller")
287 # Destroy the controller (via CLI)
288 self
.log
.debug("[reset] Destroying controller")
289 await self
.destroy_controller(cluster_uuid
)
291 self
.log
.debug("[reset] Removing k8s cloud")
292 k8s_cloud
= "k8s-{}".format(cluster_uuid
)
293 await self
.remove_cloud(k8s_cloud
)
295 except Exception as ex
:
296 self
.log
.debug("Caught exception during reset: {}".format(ex
))
307 timeout
: float = 300,
309 db_dict
: dict = None,
310 kdu_name
: str = None,
311 namespace
: str = None
315 :param cluster_uuid str: The UUID of the cluster to install to
316 :param kdu_model str: The name or path of a bundle to install
317 :param atomic bool: If set, waits until the model is active and resets
318 the cluster on failure.
319 :param timeout int: The time, in seconds, to wait for the install
321 :param params dict: Key-value pairs of instantiation parameters
322 :param kdu_name: Name of the KDU instance to be installed
323 :param namespace: K8s namespace to use for the KDU instance
325 :return: If successful, returns ?
328 if not self
.authenticated
:
329 self
.log
.debug("[install] Logging in to the controller")
330 await self
.login(cluster_uuid
)
333 # Get or create the model, based on the NS
336 kdu_instance
= "{}-{}".format(kdu_name
, db_dict
["filter"]["_id"])
338 kdu_instance
= db_dict
["filter"]["_id"]
340 self
.log
.debug("Checking for model named {}".format(kdu_instance
))
342 # Create the new model
343 self
.log
.debug("Adding model: {}".format(kdu_instance
))
344 model
= await self
.add_model(kdu_instance
, cluster_uuid
=cluster_uuid
)
347 # TODO: Instantiation parameters
350 "Juju bundle that models the KDU, in any of the following ways:
351 - <juju-repo>/<juju-bundle>
352 - <juju-bundle folder under k8s_models folder in the package>
353 - <juju-bundle tgz file (w/ or w/o extension) under k8s_models folder in the package>
354 - <URL_where_to_fetch_juju_bundle>
357 previous_workdir
= os
.getcwd()
360 if kdu_model
.startswith("cs:"):
362 elif kdu_model
.startswith("http"):
366 new_workdir
= kdu_model
.strip(kdu_model
.split("/")[-1])
368 os
.chdir(new_workdir
)
370 bundle
= "local:{}".format(kdu_model
)
373 # Raise named exception that the bundle could not be found
376 self
.log
.debug("[install] deploying {}".format(bundle
))
377 await model
.deploy(bundle
)
379 # Get the application
381 # applications = model.applications
382 self
.log
.debug("[install] Applications: {}".format(model
.applications
))
383 for name
in model
.applications
:
384 self
.log
.debug("[install] Waiting for {} to settle".format(name
))
385 application
= model
.applications
[name
]
387 # It's not enough to wait for all units to be active;
388 # the application status needs to be active as well.
389 self
.log
.debug("Waiting for all units to be active...")
390 await model
.block_until(
392 unit
.agent_status
== 'idle'
393 and application
.status
in ['active', 'unknown']
394 and unit
.workload_status
in [
396 ] for unit
in application
.units
400 self
.log
.debug("All units active.")
402 except concurrent
.futures
._base
.TimeoutError
: # TODO use asyncio.TimeoutError
403 os
.chdir(previous_workdir
)
404 self
.log
.debug("[install] Timeout exceeded; resetting cluster")
405 await self
.reset(cluster_uuid
)
408 # Wait for the application to be active
409 if model
.is_connected():
410 self
.log
.debug("[install] Disconnecting model")
411 await model
.disconnect()
413 os
.chdir(previous_workdir
)
416 raise Exception("Unable to install")
418 async def instances_list(
423 returns a list of deployed releases in a cluster
425 :param cluster_uuid: the cluster
434 kdu_model
: str = None,
439 :param cluster_uuid str: The UUID of the cluster to upgrade
440 :param kdu_instance str: The unique name of the KDU instance
441 :param kdu_model str: The name or path of the bundle to upgrade to
442 :param params dict: Key-value pairs of instantiation parameters
444 :return: If successful, reference to the new revision number of the
448 # TODO: Loop through the bundle and upgrade each charm individually
451 The API doesn't have a concept of bundle upgrades, because there are
452 many possible changes: charm revision, disk, number of units, etc.
454 As such, we are only supporting a limited subset of upgrades. We'll
455 upgrade the charm revision but leave storage and scale untouched.
457 Scale changes should happen through OSM constructs, and changes to
458 storage would require a redeployment of the service, at least in this
461 namespace
= self
.get_namespace(cluster_uuid
)
462 model
= await self
.get_model(namespace
, cluster_uuid
=cluster_uuid
)
464 with
open(kdu_model
, 'r') as f
:
465 bundle
= yaml
.safe_load(f
)
469 'description': 'Test bundle',
470 'bundle': 'kubernetes',
473 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
476 'password': 'manopw',
477 'root_password': 'osm4u',
480 'series': 'kubernetes'
485 # TODO: This should be returned in an agreed-upon format
486 for name
in bundle
['applications']:
487 self
.log
.debug(model
.applications
)
488 application
= model
.applications
[name
]
489 self
.log
.debug(application
)
491 path
= bundle
['applications'][name
]['charm']
494 await application
.upgrade_charm(switch
=path
)
495 except juju
.errors
.JujuError
as ex
:
496 if 'already running charm' in str(ex
):
497 # We're already running this version
500 await model
.disconnect()
503 raise NotImplemented()
514 :param cluster_uuid str: The UUID of the cluster to rollback
515 :param kdu_instance str: The unique name of the KDU instance
516 :param revision int: The revision to revert to. If omitted, rolls back
517 the previous upgrade.
519 :return: If successful, returns the revision of active KDU instance,
520 or raises an exception
522 raise NotImplemented()
530 """Uninstall a KDU instance
532 :param cluster_uuid str: The UUID of the cluster
533 :param kdu_instance str: The unique name of the KDU instance
535 :return: Returns True if successful, or raises an exception
537 if not self
.authenticated
:
538 self
.log
.debug("[uninstall] Connecting to controller")
539 await self
.login(cluster_uuid
)
541 self
.log
.debug("[uninstall] Destroying model")
543 await self
.controller
.destroy_models(kdu_instance
)
545 self
.log
.debug("[uninstall] Model destroyed and disconnecting")
550 async def exec_primitive(
552 cluster_uuid
: str = None,
553 kdu_instance
: str = None,
554 primitive_name
: str = None,
555 timeout
: float = 300,
557 db_dict
: dict = None,
559 """Exec primitive (Juju action)
561 :param cluster_uuid str: The UUID of the cluster
562 :param kdu_instance str: The unique name of the KDU instance
563 :param primitive_name: Name of action that will be executed
564 :param timeout: Timeout for action execution
565 :param params: Dictionary of all the parameters needed for the action
566 :db_dict: Dictionary for any additional data
568 :return: Returns the output of the action
570 if not self
.authenticated
:
571 self
.log
.debug("[exec_primitive] Connecting to controller")
572 await self
.login(cluster_uuid
)
574 if not params
or "application-name" not in params
:
575 raise K8sException("Missing application-name argument, \
576 argument needed for K8s actions")
578 self
.log
.debug("[exec_primitive] Getting model "
579 "kdu_instance: {}".format(kdu_instance
))
581 model
= await self
.get_model(kdu_instance
, cluster_uuid
)
583 application_name
= params
["application-name"]
584 application
= model
.applications
[application_name
]
586 actions
= await application
.get_actions()
587 if primitive_name
not in actions
:
588 raise K8sException("Primitive {} not found".format(primitive_name
))
591 for u
in application
.units
:
592 if await u
.is_leader_from_status():
597 raise K8sException("No leader unit found to execute action")
599 self
.log
.debug("[exec_primitive] Running action: {}".format(primitive_name
))
600 action
= await unit
.run_action(primitive_name
, **params
)
602 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
603 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
606 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
609 if status
!= "completed":
610 raise K8sException("status is not completed: {} output: {}".format(status
, output
))
614 except Exception as e
:
615 error_msg
= "Error executing primitive {}: {}".format(primitive_name
, e
)
616 self
.log
.error(error_msg
)
617 raise K8sException(message
=error_msg
)
620 async def inspect_kdu(
626 Inspects a bundle and returns a dictionary of config parameters and
627 their default values.
629 :param kdu_model str: The name or path of the bundle to inspect.
631 :return: If successful, returns a dictionary of available parameters
632 and their default values.
636 with
open(kdu_model
, 'r') as f
:
637 bundle
= yaml
.safe_load(f
)
641 'description': 'Test bundle',
642 'bundle': 'kubernetes',
645 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
648 'password': 'manopw',
649 'root_password': 'osm4u',
652 'series': 'kubernetes'
657 # TODO: This should be returned in an agreed-upon format
658 kdu
= bundle
['applications']
668 If available, returns the README of the bundle.
670 :param kdu_model str: The name or path of a bundle
672 :return: If found, returns the contents of the README.
676 files
= ['README', 'README.txt', 'README.md']
677 path
= os
.path
.dirname(kdu_model
)
678 for file in os
.listdir(path
):
680 with
open(file, 'r') as f
:
686 async def status_kdu(
691 """Get the status of the KDU
693 Get the current status of the KDU instance.
695 :param cluster_uuid str: The UUID of the cluster
696 :param kdu_instance str: The unique id of the KDU instance
698 :return: Returns a dictionary containing namespace, state, resources,
703 model
= await self
.get_model(self
.get_namespace(cluster_uuid
), cluster_uuid
=cluster_uuid
)
705 # model = await self.get_model_by_uuid(cluster_uuid)
707 model_status
= await model
.get_status()
708 status
= model_status
.applications
710 for name
in model_status
.applications
:
711 application
= model_status
.applications
[name
]
713 'status': application
['status']['status']
716 if model
.is_connected():
717 await model
.disconnect()
727 """Add a k8s cloud to Juju
729 Adds a Kubernetes cloud to Juju, so it can be bootstrapped with a
732 :param cloud_name str: The name of the cloud to add.
733 :param credentials dict: A dictionary representing the output of
734 `kubectl config view --raw`.
736 :returns: True if successful, otherwise raises an exception.
739 cmd
= [self
.juju_command
, "add-k8s", "--local", cloud_name
]
742 process
= await asyncio
.create_subprocess_exec(
744 stdout
=asyncio
.subprocess
.PIPE
,
745 stderr
=asyncio
.subprocess
.PIPE
,
746 stdin
=asyncio
.subprocess
.PIPE
,
749 # Feed the process the credentials
750 process
.stdin
.write(credentials
.encode("utf-8"))
751 await process
.stdin
.drain()
752 process
.stdin
.close()
754 stdout
, stderr
= await process
.communicate()
756 return_code
= process
.returncode
758 self
.log
.debug("add-k8s return code: {}".format(return_code
))
761 raise Exception(stderr
)
769 ) -> juju
.model
.Model
:
770 """Adds a model to the controller
772 Adds a new model to the Juju controller
774 :param model_name str: The name of the model to add.
775 :returns: The juju.model.Model object of the new model upon success or
778 if not self
.authenticated
:
779 await self
.login(cluster_uuid
)
781 self
.log
.debug("Adding model '{}' to cluster_uuid '{}'".format(model_name
, cluster_uuid
))
783 model
= await self
.controller
.add_model(
785 config
={'authorized-keys': self
.juju_public_key
}
787 except Exception as ex
:
789 self
.log
.debug("Caught exception: {}".format(ex
))
800 """Bootstrap a Kubernetes controller
802 Bootstrap a Juju controller inside the Kubernetes cluster
804 :param cloud_name str: The name of the cloud.
805 :param cluster_uuid str: The UUID of the cluster to bootstrap.
806 :param loadbalancer bool: If the controller should use loadbalancer or not.
807 :returns: True upon success or raises an exception.
811 cmd
= [self
.juju_command
, "bootstrap", cloud_name
, cluster_uuid
]
814 For public clusters, specify that the controller service is using a LoadBalancer.
816 cmd
= [self
.juju_command
, "bootstrap", cloud_name
, cluster_uuid
, "--config", "controller-service-type=loadbalancer"]
818 self
.log
.debug("Bootstrapping controller {} in cloud {}".format(
819 cluster_uuid
, cloud_name
822 process
= await asyncio
.create_subprocess_exec(
824 stdout
=asyncio
.subprocess
.PIPE
,
825 stderr
=asyncio
.subprocess
.PIPE
,
828 stdout
, stderr
= await process
.communicate()
830 return_code
= process
.returncode
834 if b
'already exists' not in stderr
:
835 raise Exception(stderr
)
839 async def destroy_controller(
843 """Destroy a Kubernetes controller
845 Destroy an existing Kubernetes controller.
847 :param cluster_uuid str: The UUID of the cluster to bootstrap.
848 :returns: True upon success or raises an exception.
852 "destroy-controller",
853 "--destroy-all-models",
859 process
= await asyncio
.create_subprocess_exec(
861 stdout
=asyncio
.subprocess
.PIPE
,
862 stderr
=asyncio
.subprocess
.PIPE
,
865 stdout
, stderr
= await process
.communicate()
867 return_code
= process
.returncode
871 if 'already exists' not in stderr
:
872 raise Exception(stderr
)
878 """Get the cluster configuration
880 Gets the configuration of the cluster
882 :param cluster_uuid str: The UUID of the cluster.
883 :return: A dict upon success, or raises an exception.
885 cluster_config
= "{}/{}.yaml".format(self
.fs
.path
, cluster_uuid
)
886 if os
.path
.exists(cluster_config
):
887 with
open(cluster_config
, 'r') as f
:
888 config
= yaml
.safe_load(f
.read())
892 "Unable to locate configuration for cluster {}".format(
901 ) -> juju
.model
.Model
:
902 """Get a model from the Juju Controller.
904 Note: Model objects returned must call disconnected() before it goes
907 :param model_name str: The name of the model to get
908 :return The juju.model.Model object if found, or None.
910 if not self
.authenticated
:
911 await self
.login(cluster_uuid
)
914 models
= await self
.controller
.list_models()
915 if model_name
in models
:
916 self
.log
.debug("Found model: {}".format(model_name
))
917 model
= await self
.controller
.get_model(
926 """Get the namespace UUID
927 Gets the namespace's unique name
929 :param cluster_uuid str: The UUID of the cluster
930 :returns: The namespace UUID, or raises an exception
932 config
= self
.get_config(cluster_uuid
)
934 # Make sure the name is in the config
935 if 'namespace' not in config
:
936 raise Exception("Namespace not found.")
938 # TODO: We want to make sure this is unique to the cluster, in case
939 # the cluster is being reused.
940 # Consider pre/appending the cluster id to the namespace string
941 return config
['namespace']
947 """Check if a model exists in the controller
949 Checks to see if a model exists in the connected Juju controller.
951 :param model_name str: The name of the model
952 :return: A boolean indicating if the model exists
954 models
= await self
.controller
.list_models()
956 if model_name
in models
:
964 """Check if a cluster is local
966 Checks if a cluster is running in the local host
968 :param credentials dict: A dictionary containing the k8s credentials
969 :returns: A boolean if the cluster is running locally
971 creds
= yaml
.safe_load(credentials
)
972 if os
.getenv("OSMLCM_VCA_APIPROXY"):
973 host_ip
= os
.getenv("OSMLCM_VCA_APIPROXY")
975 if creds
and host_ip
:
976 for cluster
in creds
['clusters']:
977 if 'server' in cluster
['cluster']:
978 if host_ip
in cluster
['cluster']['server']:
983 async def login(self
, cluster_uuid
):
984 """Login to the Juju controller."""
986 if self
.authenticated
:
989 self
.connecting
= True
991 # Test: Make sure we have the credentials loaded
992 config
= self
.get_config(cluster_uuid
)
994 self
.juju_endpoint
= config
['endpoint']
995 self
.juju_user
= config
['username']
996 self
.juju_secret
= config
['secret']
997 self
.juju_ca_cert
= config
['cacert']
998 self
.juju_public_key
= None
1000 self
.controller
= Controller()
1002 if self
.juju_secret
:
1004 "Connecting to controller... ws://{} as {}/{}".format(
1011 await self
.controller
.connect(
1012 endpoint
=self
.juju_endpoint
,
1013 username
=self
.juju_user
,
1014 password
=self
.juju_secret
,
1015 cacert
=self
.juju_ca_cert
,
1017 self
.authenticated
= True
1018 self
.log
.debug("JujuApi: Logged into controller")
1019 except Exception as ex
:
1021 self
.log
.debug("Caught exception: {}".format(ex
))
1024 self
.log
.fatal("VCA credentials not configured.")
1025 self
.authenticated
= False
1027 async def logout(self
):
1028 """Logout of the Juju controller."""
1029 self
.log
.debug("[logout]")
1030 if not self
.authenticated
:
1033 for model
in self
.models
:
1034 self
.log
.debug("Logging out of model {}".format(model
))
1035 await self
.models
[model
].disconnect()
1038 self
.log
.debug("Disconnecting controller {}".format(
1041 await self
.controller
.disconnect()
1042 self
.controller
= None
1044 self
.authenticated
= False
1046 async def remove_cloud(
1050 """Remove a k8s cloud from Juju
1052 Removes a Kubernetes cloud from Juju.
1054 :param cloud_name str: The name of the cloud to add.
1056 :returns: True if successful, otherwise raises an exception.
1059 # Remove the bootstrapped controller
1060 cmd
= [self
.juju_command
, "remove-k8s", "--client", cloud_name
]
1061 process
= await asyncio
.create_subprocess_exec(
1063 stdout
=asyncio
.subprocess
.PIPE
,
1064 stderr
=asyncio
.subprocess
.PIPE
,
1067 stdout
, stderr
= await process
.communicate()
1069 return_code
= process
.returncode
1072 raise Exception(stderr
)
1074 # Remove the cloud from the local config
1075 cmd
= [self
.juju_command
, "remove-cloud", "--client", cloud_name
]
1076 process
= await asyncio
.create_subprocess_exec(
1078 stdout
=asyncio
.subprocess
.PIPE
,
1079 stderr
=asyncio
.subprocess
.PIPE
,
1082 stdout
, stderr
= await process
.communicate()
1084 return_code
= process
.returncode
1087 raise Exception(stderr
)
1091 async def set_config(
1096 """Save the cluster configuration
1098 Saves the cluster information to the file store
1100 :param cluster_uuid str: The UUID of the cluster
1101 :param config dict: A dictionary containing the cluster configuration
1102 :returns: Boolean upon success or raises an exception.
1105 cluster_config
= "{}/{}.yaml".format(self
.fs
.path
, cluster_uuid
)
1106 if not os
.path
.exists(cluster_config
):
1107 self
.log
.debug("Writing config to {}".format(cluster_config
))
1108 with
open(cluster_config
, 'w') as f
:
1109 f
.write(yaml
.dump(config
, Dumper
=yaml
.Dumper
))