import contextlib import os import shutil import ssl import urllib from urllib.request import urlopen from dcos import config, constants, http, util from dcos.errors import DCOSException logger = util.get_logger(__name__) def move_to_cluster_config(): """Create a cluster specific config file + directory from a global config file. This will move users from global config structure (~/.dcos/dcos.toml) to the cluster specific one (~/.dcos/clusters/CLUSTER_ID/dcos.toml) and set that cluster as the "attached" cluster. :rtype: None """ global_config = config.get_global_config() dcos_url = config.get_config_val("core.dcos_url", global_config) # if no cluster is set, do not move the cluster yet if dcos_url is None: return try: # find cluster id cluster_url = dcos_url.rstrip('/') + '/metadata' res = http.get(cluster_url, timeout=1) cluster_id = res.json().get("CLUSTER_ID") # don't move cluster if dcos_url is not valid except DCOSException as e: logger.error( "Error trying to find cluster id: {}".format(e)) return # create cluster id dir cluster_path = os.path.join(config.get_config_dir_path(), constants.DCOS_CLUSTERS_SUBDIR, cluster_id) util.ensure_dir_exists(cluster_path) # move config file to new location global_config_path = config.get_global_config_path() util.sh_copy(global_config_path, cluster_path) # set cluster as attached util.ensure_file_exists(os.path.join( cluster_path, constants.DCOS_CLUSTER_ATTACHED_FILE)) @contextlib.contextmanager def setup_directory(): """ A context manager for the temporary setup directory created as a placeholder before we find the cluster's CLUSTER_ID. :returns: path of setup directory :rtype: str """ try: temp_path = os.path.join(config.get_config_dir_path(), constants.DCOS_CLUSTERS_SUBDIR, "setup") util.ensure_dir_exists(temp_path) yield temp_path finally: shutil.rmtree(temp_path, ignore_errors=True) def setup_cluster_config(dcos_url, temp_path, stored_cert): """ Create a cluster directory for cluster specified in "temp_path" directory. :param dcos_url: url to DC/OS cluster :type dcos_url: str :param temp_path: path to temporary config dir :type temp_path: str :param stored_cert: whether we stored cert bundle in 'setup' dir :type stored_cert: bool :returns: path to cluster specific directory :rtype: str """ try: # find cluster id cluster_url = dcos_url.rstrip('/') + '/metadata' res = http.get(cluster_url, timeout=1) cluster_id = res.json().get("CLUSTER_ID") except DCOSException as e: msg = ("Error trying to find cluster id: {}\n " "Please make sure the provided DC/OS URL is valid: {}".format( e, dcos_url)) raise DCOSException(msg) # create cluster id dir cluster_path = os.path.join(config.get_config_dir_path(), constants.DCOS_CLUSTERS_SUBDIR, cluster_id) if os.path.exists(cluster_path): raise DCOSException("Cluster [{}] is already setup".format(dcos_url)) util.ensure_dir_exists(cluster_path) # move contents of setup dir to new location for (path, dirnames, filenames) in os.walk(temp_path): for f in filenames: util.sh_copy(os.path.join(path, f), cluster_path) cluster = Cluster(cluster_id) config_path = cluster.get_config_path() if stored_cert: cert_path = os.path.join(cluster_path, "dcos_ca.crt") config.set_val("core.ssl_verify", cert_path, config_path=config_path) cluster_name = cluster_id try: url = dcos_url.rstrip('/') + '/mesos/state-summary' name_query = http.get(url, timeout=1, toml_config=cluster.get_config()) cluster_name = name_query.json().get("cluster") except DCOSException: pass config.set_val("cluster.name", cluster_name, config_path=config_path) return cluster_path def set_attached(cluster_path): """ Set the cluster specified in `cluster_path` as the attached cluster :param cluster_path: path to cluster directory :type cluster_path: str :rtype: None """ # get currently attached cluster attached_cluster_path = config.get_attached_cluster_path() if attached_cluster_path and attached_cluster_path != cluster_path: # set cluster as attached attached_file = os.path.join( attached_cluster_path, constants.DCOS_CLUSTER_ATTACHED_FILE) try: util.sh_move(attached_file, cluster_path) except DCOSException as e: msg = "Failed to attach cluster: {}".format(e) raise DCOSException(msg) else: util.ensure_file_exists(os.path.join( cluster_path, constants.DCOS_CLUSTER_ATTACHED_FILE)) def get_cluster_cert(dcos_url): """Get CA bundle from specified cluster. This is an insecure request. :param dcos_url: url to DC/OS cluster :type dcos_url: str :returns: cert :rtype: str """ cert_bundle_url = dcos_url.rstrip() + "/ca/dcos-ca.crt" unverified = ssl.create_default_context() unverified.check_hostname = False unverified.verify_mode = ssl.CERT_NONE error_msg = ("Error downloading CA certificate from cluster. " "Please check the provided DC/OS URL.") try: with urlopen(cert_bundle_url, context=unverified) as f: return f.read().decode('utf-8') except urllib.error.HTTPError as e: # Open source DC/OS does not currently expose its root CA certificate if e.code == 404: return False else: logger.debug(e) raise DCOSException(error_msg) except Exception as e: logger.debug(e) raise DCOSException(error_msg) def get_clusters(): """ :returns: list of configured Clusters :rtype: [Clusters] """ clusters_path = config.get_clusters_path() util.ensure_dir_exists(clusters_path) clusters = os.listdir(clusters_path) return [Cluster(cluster_id) for cluster_id in clusters] def get_cluster(name): """ :param name: name of cluster :type name: str :returns: Cluster identified by name :rtype: Cluster """ return next((c for c in get_clusters() if c.get_cluster_id() == name or c.get_name() == name), None) def remove(name): """ Remove cluster `name` from the CLI. :param name: name of cluster :type name: str :rtype: None """ def onerror(func, path, excinfo): raise DCOSException("Error trying to remove cluster") cluster = get_cluster(name) if cluster: shutil.rmtree(cluster.get_cluster_path(), onerror) return else: raise DCOSException("Cluster [{}] does not exist".format(name)) class Cluster(): """Interface for a configured cluster""" def __init__(self, cluster_id): self.cluster_id = cluster_id self.cluster_path = os.path.join( config.get_clusters_path(), cluster_id) def get_cluster_path(self): return self.cluster_path def get_cluster_id(self): return self.cluster_id def get_config_path(self): return os.path.join(self.cluster_path, "dcos.toml") def get_config(self, mutable=False): return config.load_from_path(self.get_config_path(), mutable) def get_name(self): return config.get_config_val( "cluster.name", self.get_config()) or self.cluster_id def get_url(self): return config.get_config_val("core.dcos_url", self.get_config()) def get_dcos_version(self): dcos_url = self.get_url() if dcos_url: url = os.path.join( self.get_url(), "dcos-metadata/dcos-version.json") try: resp = http.get(url, timeout=1, toml_config=self.get_config()) return resp.json().get("version", "N/A") except DCOSException: pass return "N/A" def is_attached(self): return os.path.exists(os.path.join( self.cluster_path, constants.DCOS_CLUSTER_ATTACHED_FILE)) def __eq__(self, other): return isinstance(other, Cluster) and \ other.get_cluster_id() == self.get_cluster_id() def dict(self): return { "cluster_id": self.get_cluster_id(), "name": self.get_name(), "url": self.get_url(), "version": self.get_dcos_version(), "attached": self.is_attached() }