X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=juju%2Fclient%2Fjujudata.py;fp=juju%2Fclient%2Fjujudata.py;h=8b844c28b63cafd737a0ae0f768cee1df2870312;hp=0000000000000000000000000000000000000000;hb=b8a8281b1785358bd5632a119c016f21811172c6;hpb=dcdf82bbc1ef310379f746518b2dd3b006353cb3 diff --git a/juju/client/jujudata.py b/juju/client/jujudata.py new file mode 100644 index 0000000..8b844c2 --- /dev/null +++ b/juju/client/jujudata.py @@ -0,0 +1,219 @@ +import abc +import io +import os +import pathlib + +import juju.client.client as jujuclient +import yaml +from juju import tag +from juju.client.gocookies import GoCookieJar +from juju.errors import JujuError + + +class NoModelException(Exception): + pass + + +class JujuData: + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def current_controller(self): + '''Return the current controller name''' + raise NotImplementedError() + + @abc.abstractmethod + def controllers(self): + '''Return all the currently known controllers as a dict + mapping controller name to a dict containing the + following string keys: + uuid: The UUID of the controller + api-endpoints: A list of host:port addresses for the controller. + ca-cert: the PEM-encoded CA cert of the controller (optional) + + This is compatible with the "controllers" entry in the YAML-unmarshaled data + stored in ~/.local/share/juju/controllers.yaml. + ''' + raise NotImplementedError() + + @abc.abstractmethod + def models(self): + '''Return all the currently known models as a dict + containing a key for each known controller, + each holding a dict value containing an optional "current-model" + key (the name of the current model for that controller, + if there is one), and a dict mapping fully-qualified + model names to a dict containing a "uuid" key with the + key for that model. + This is compatible with the YAML-unmarshaled data + stored in ~/.local/share/juju/models.yaml. + ''' + raise NotImplementedError() + + @abc.abstractmethod + def accounts(self): + '''Return the currently known accounts, as a dict + containing a key for each known controller, with + each value holding a dict with the following keys: + + user: The username to use when logging into the controller (str) + password: The password to use when logging into the controller (str, optional) + ''' + raise NotImplementedError() + + @abc.abstractmethod + def cookies_for_controller(self, controller_name): + '''Return the cookie jar to use when connecting to the + controller with the given name. + :return http.cookiejar.CookieJar + ''' + raise NotImplementedError() + + @abc.abstractmethod + def current_model(self, controller_name=None, model_only=False): + '''Return the current model, qualified by its controller name. + If controller_name is specified, the current model for + that controller will be returned. + If model_only is true, only the model name, not qualified by + its controller name, will be returned. + ''' + raise NotImplementedError() + + def parse_model(self, model): + """Split the given model_name into controller and model parts. + If the controller part is empty, the current controller will be used. + If the model part is empty, the current model will be used for + the controller. + The returned model name will always be qualified with a username. + :param model str: The model name to parse. + :return (str, str): The controller and model names. + """ + # TODO if model is empty, use $JUJU_MODEL environment variable. + if model and ':' in model: + # explicit controller given + controller_name, model_name = model.split(':') + else: + # use the current controller if one isn't explicitly given + controller_name = self.current_controller() + model_name = model + if not controller_name: + controller_name = self.current_controller() + if not model_name: + model_name = self.current_model(controller_name, model_only=True) + if not model_name: + raise NoModelException('no current model') + + if '/' not in model_name: + # model name doesn't include a user prefix, so add one + # by using the current user for the controller. + accounts = self.accounts().get(controller_name) + if accounts is None: + raise JujuError('No account found for controller {} '.format(controller_name)) + username = accounts.get('user') + if username is None: + raise JujuError('No username found for controller {}'.format(controller_name)) + model_name = username + "/" + model_name + + return controller_name, model_name + + +class FileJujuData(JujuData): + '''Provide access to the Juju client configuration files. + Any configuration file is read once and then cached.''' + def __init__(self): + self.path = os.environ.get('JUJU_DATA') or '~/.local/share/juju' + self.path = os.path.abspath(os.path.expanduser(self.path)) + # _loaded keeps track of the loaded YAML from + # the Juju data files so we don't need to load the same + # file many times. + self._loaded = {} + + def refresh(self): + '''Forget the cache of configuration file data''' + self._loaded = {} + + def current_controller(self): + '''Return the current controller name''' + return self._load_yaml('controllers.yaml', 'current-controller') + + def current_model(self, controller_name=None, model_only=False): + '''Return the current model, qualified by its controller name. + If controller_name is specified, the current model for + that controller will be returned. + + If model_only is true, only the model name, not qualified by + its controller name, will be returned. + ''' + # TODO respect JUJU_MODEL environment variable. + if not controller_name: + controller_name = self.current_controller() + if not controller_name: + raise JujuError('No current controller') + models = self.models()[controller_name] + if 'current-model' not in models: + return None + if model_only: + return models['current-model'] + return controller_name + ':' + models['current-model'] + + def load_credential(self, cloud, name=None): + """Load a local credential. + + :param str cloud: Name of cloud to load credentials from. + :param str name: Name of credential. If None, the default credential + will be used, if available. + :return: A CloudCredential instance, or None. + """ + try: + cloud = tag.untag('cloud-', cloud) + creds_data = self.credentials()[cloud] + if not name: + default_credential = creds_data.pop('default-credential', None) + default_region = creds_data.pop('default-region', None) # noqa + if default_credential: + name = creds_data['default-credential'] + elif len(creds_data) == 1: + name = list(creds_data)[0] + else: + return None, None + cred_data = creds_data[name] + auth_type = cred_data.pop('auth-type') + return name, jujuclient.CloudCredential( + auth_type=auth_type, + attrs=cred_data, + ) + except (KeyError, FileNotFoundError): + return None, None + + def controllers(self): + return self._load_yaml('controllers.yaml', 'controllers') + + def models(self): + return self._load_yaml('models.yaml', 'controllers') + + def accounts(self): + return self._load_yaml('accounts.yaml', 'controllers') + + def credentials(self): + return self._load_yaml('credentials.yaml', 'credentials') + + def _load_yaml(self, filename, key): + if filename in self._loaded: + # Data already exists in the cache. + return self._loaded[filename].get(key) + # TODO use the file lock like Juju does. + filepath = os.path.join(self.path, filename) + with io.open(filepath, 'rt') as f: + data = yaml.safe_load(f) + self._loaded[filename] = data + return data.get(key) + + def cookies_for_controller(self, controller_name): + f = pathlib.Path(self.path) / 'cookies' / (controller_name + '.json') + if not f.exists(): + f = pathlib.Path('~/.go-cookies').expanduser() + # TODO if neither cookie file exists, where should + # we create the cookies? + jar = GoCookieJar(str(f)) + jar.load() + return jar