6 import juju
.client
.client
as jujuclient
9 from juju
.client
.gocookies
import GoCookieJar
10 from juju
.errors
import JujuError
13 class NoModelException(Exception):
18 __metaclass__
= abc
.ABCMeta
21 def current_controller(self
):
22 '''Return the current controller name'''
23 raise NotImplementedError()
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)
34 This is compatible with the "controllers" entry in the YAML-unmarshaled data
35 stored in ~/.local/share/juju/controllers.yaml.
37 raise NotImplementedError()
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
48 This is compatible with the YAML-unmarshaled data
49 stored in ~/.local/share/juju/models.yaml.
51 raise NotImplementedError()
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:
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)
62 raise NotImplementedError()
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
70 raise NotImplementedError()
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.
80 raise NotImplementedError()
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
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.
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(':')
96 # use the current controller if one isn't explicitly given
97 controller_name
= self
.current_controller()
99 if not controller_name
:
100 controller_name
= self
.current_controller()
102 model_name
= self
.current_model(controller_name
, model_only
=True)
104 raise NoModelException('no current model')
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
)
111 raise JujuError('No account found for controller {} '.format(controller_name
))
112 username
= accounts
.get('user')
114 raise JujuError('No username found for controller {}'.format(controller_name
))
115 model_name
= username
+ "/" + model_name
117 return controller_name
, model_name
120 class FileJujuData(JujuData
):
121 '''Provide access to the Juju client configuration files.
122 Any configuration file is read once and then cached.'''
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
132 '''Forget the cache of configuration file data'''
135 def current_controller(self
):
136 '''Return the current controller name'''
137 return self
._load
_yaml
('controllers.yaml', 'current-controller')
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.
144 If model_only is true, only the model name, not qualified by
145 its controller name, will be returned.
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
:
156 return models
['current-model']
157 return controller_name
+ ':' + models
['current-model']
159 def load_credential(self
, cloud
, name
=None):
160 """Load a local credential.
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.
168 cloud
= tag
.untag('cloud-', cloud
)
169 creds_data
= self
.credentials()[cloud
]
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]
179 cred_data
= creds_data
[name
]
180 auth_type
= cred_data
.pop('auth-type')
181 return name
, jujuclient
.CloudCredential(
185 except (KeyError, FileNotFoundError
):
188 def controllers(self
):
189 return self
._load
_yaml
('controllers.yaml', 'controllers')
192 return self
._load
_yaml
('models.yaml', 'controllers')
195 return self
._load
_yaml
('accounts.yaml', 'controllers')
197 def credentials(self
):
198 return self
._load
_yaml
('credentials.yaml', 'credentials')
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
211 def cookies_for_controller(self
, controller_name
):
212 f
= pathlib
.Path(self
.path
) / 'cookies' / (controller_name
+ '.json')
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
))