Merge "Fix bug 564"
[osm/N2VC.git] / modules / libjuju / juju / client / jujudata.py
1 import abc
2 import io
3 import os
4 import pathlib
5
6 import juju.client.client as jujuclient
7 import yaml
8 from juju import tag
9 from juju.client.gocookies import GoCookieJar
10 from juju.errors import JujuError
11
12
13 class NoModelException(Exception):
14 pass
15
16
17 class JujuData:
18 __metaclass__ = abc.ABCMeta
19
20 @abc.abstractmethod
21 def current_controller(self):
22 '''Return the current controller name'''
23 raise NotImplementedError()
24
25 @abc.abstractmethod
26 def controllers(self):
27 '''Return all the currently known controllers as a dict
28 mapping controller name to a dict containing the
29 following string keys:
30 uuid: The UUID of the controller
31 api-endpoints: A list of host:port addresses for the controller.
32 ca-cert: the PEM-encoded CA cert of the controller (optional)
33
34 This is compatible with the "controllers" entry in the YAML-unmarshaled data
35 stored in ~/.local/share/juju/controllers.yaml.
36 '''
37 raise NotImplementedError()
38
39 @abc.abstractmethod
40 def models(self):
41 '''Return all the currently known models as a dict
42 containing a key for each known controller,
43 each holding a dict value containing an optional "current-model"
44 key (the name of the current model for that controller,
45 if there is one), and a dict mapping fully-qualified
46 model names to a dict containing a "uuid" key with the
47 key for that model.
48 This is compatible with the YAML-unmarshaled data
49 stored in ~/.local/share/juju/models.yaml.
50 '''
51 raise NotImplementedError()
52
53 @abc.abstractmethod
54 def accounts(self):
55 '''Return the currently known accounts, as a dict
56 containing a key for each known controller, with
57 each value holding a dict with the following keys:
58
59 user: The username to use when logging into the controller (str)
60 password: The password to use when logging into the controller (str, optional)
61 '''
62 raise NotImplementedError()
63
64 @abc.abstractmethod
65 def cookies_for_controller(self, controller_name):
66 '''Return the cookie jar to use when connecting to the
67 controller with the given name.
68 :return http.cookiejar.CookieJar
69 '''
70 raise NotImplementedError()
71
72 @abc.abstractmethod
73 def current_model(self, controller_name=None, model_only=False):
74 '''Return the current model, qualified by its controller name.
75 If controller_name is specified, the current model for
76 that controller will be returned.
77 If model_only is true, only the model name, not qualified by
78 its controller name, will be returned.
79 '''
80 raise NotImplementedError()
81
82 def parse_model(self, model):
83 """Split the given model_name into controller and model parts.
84 If the controller part is empty, the current controller will be used.
85 If the model part is empty, the current model will be used for
86 the controller.
87 The returned model name will always be qualified with a username.
88 :param model str: The model name to parse.
89 :return (str, str): The controller and model names.
90 """
91 # TODO if model is empty, use $JUJU_MODEL environment variable.
92 if model and ':' in model:
93 # explicit controller given
94 controller_name, model_name = model.split(':')
95 else:
96 # use the current controller if one isn't explicitly given
97 controller_name = self.current_controller()
98 model_name = model
99 if not controller_name:
100 controller_name = self.current_controller()
101 if not model_name:
102 model_name = self.current_model(controller_name, model_only=True)
103 if not model_name:
104 raise NoModelException('no current model')
105
106 if '/' not in model_name:
107 # model name doesn't include a user prefix, so add one
108 # by using the current user for the controller.
109 accounts = self.accounts().get(controller_name)
110 if accounts is None:
111 raise JujuError('No account found for controller {} '.format(controller_name))
112 username = accounts.get('user')
113 if username is None:
114 raise JujuError('No username found for controller {}'.format(controller_name))
115 model_name = username + "/" + model_name
116
117 return controller_name, model_name
118
119
120 class FileJujuData(JujuData):
121 '''Provide access to the Juju client configuration files.
122 Any configuration file is read once and then cached.'''
123 def __init__(self):
124 self.path = os.environ.get('JUJU_DATA') or '~/.local/share/juju'
125 self.path = os.path.abspath(os.path.expanduser(self.path))
126 # _loaded keeps track of the loaded YAML from
127 # the Juju data files so we don't need to load the same
128 # file many times.
129 self._loaded = {}
130
131 def refresh(self):
132 '''Forget the cache of configuration file data'''
133 self._loaded = {}
134
135 def current_controller(self):
136 '''Return the current controller name'''
137 return self._load_yaml('controllers.yaml', 'current-controller')
138
139 def current_model(self, controller_name=None, model_only=False):
140 '''Return the current model, qualified by its controller name.
141 If controller_name is specified, the current model for
142 that controller will be returned.
143
144 If model_only is true, only the model name, not qualified by
145 its controller name, will be returned.
146 '''
147 # TODO respect JUJU_MODEL environment variable.
148 if not controller_name:
149 controller_name = self.current_controller()
150 if not controller_name:
151 raise JujuError('No current controller')
152 models = self.models()[controller_name]
153 if 'current-model' not in models:
154 return None
155 if model_only:
156 return models['current-model']
157 return controller_name + ':' + models['current-model']
158
159 def load_credential(self, cloud, name=None):
160 """Load a local credential.
161
162 :param str cloud: Name of cloud to load credentials from.
163 :param str name: Name of credential. If None, the default credential
164 will be used, if available.
165 :return: A CloudCredential instance, or None.
166 """
167 try:
168 cloud = tag.untag('cloud-', cloud)
169 creds_data = self.credentials()[cloud]
170 if not name:
171 default_credential = creds_data.pop('default-credential', None)
172 default_region = creds_data.pop('default-region', None) # noqa
173 if default_credential:
174 name = creds_data['default-credential']
175 elif len(creds_data) == 1:
176 name = list(creds_data)[0]
177 else:
178 return None, None
179 cred_data = creds_data[name]
180 auth_type = cred_data.pop('auth-type')
181 return name, jujuclient.CloudCredential(
182 auth_type=auth_type,
183 attrs=cred_data,
184 )
185 except (KeyError, FileNotFoundError):
186 return None, None
187
188 def controllers(self):
189 return self._load_yaml('controllers.yaml', 'controllers')
190
191 def models(self):
192 return self._load_yaml('models.yaml', 'controllers')
193
194 def accounts(self):
195 return self._load_yaml('accounts.yaml', 'controllers')
196
197 def credentials(self):
198 return self._load_yaml('credentials.yaml', 'credentials')
199
200 def _load_yaml(self, filename, key):
201 if filename in self._loaded:
202 # Data already exists in the cache.
203 return self._loaded[filename].get(key)
204 # TODO use the file lock like Juju does.
205 filepath = os.path.join(self.path, filename)
206 with io.open(filepath, 'rt') as f:
207 data = yaml.safe_load(f)
208 self._loaded[filename] = data
209 return data.get(key)
210
211 def cookies_for_controller(self, controller_name):
212 f = pathlib.Path(self.path) / 'cookies' / (controller_name + '.json')
213 if not f.exists():
214 f = pathlib.Path('~/.go-cookies').expanduser()
215 # TODO if neither cookie file exists, where should
216 # we create the cookies?
217 jar = GoCookieJar(str(f))
218 jar.load()
219 return jar