1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20
from charmhelpers.core import hookenv
21
from charmhelpers.core import host
22
from charmhelpers.core import templating
24
from charmhelpers.core.services.base import ManagerCallback
27
__all__ = ['RelationContext', 'TemplateCallback',
28
'render_template', 'template']
31
class RelationContext(dict):
33
Base class for a context generator that gets relation data from juju.
35
Subclasses must provide the attributes `name`, which is the name of the
36
interface of interest, `interface`, which is the type of the interface of
37
interest, and `required_keys`, which is the set of keys required for the
38
relation to be considered complete. The data for all interfaces matching
39
the `name` attribute that are complete will used to populate the dictionary
40
values (see `get_data`, below).
42
The generated context will be namespaced under the relation :attr:`name`,
43
to prevent potential naming conflicts.
45
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
46
:param list additional_required_keys: Extend the list of :attr:`required_keys`
51
def __init__(self, name=None, additional_required_keys=None):
52
if not hasattr(self, 'required_keys'):
53
self.required_keys = []
57
if additional_required_keys:
58
self.required_keys.extend(additional_required_keys)
63
Returns True if all of the required_keys are available.
65
return self.is_ready()
67
__nonzero__ = __bool__
70
return super(RelationContext, self).__repr__()
74
Returns True if all of the `required_keys` are available from any units.
76
ready = len(self.get(self.name, [])) > 0
78
hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
81
def _is_ready(self, unit_data):
83
Helper method that tests a set of relation data and returns True if
84
all of the `required_keys` are present.
86
return set(unit_data.keys()).issuperset(set(self.required_keys))
90
Retrieve the relation data for each unit involved in a relation and,
91
if complete, store it in a list under `self[self.name]`. This
92
is automatically called when the RelationContext is instantiated.
94
The units are sorted lexographically first by the service ID, then by
95
the unit ID. Thus, if an interface has two other services, 'db:1'
96
and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
97
and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
98
set of data, the relation data for the units will be stored in the
99
order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
101
If you only care about a single unit on the relation, you can just
102
access it as `{{ interface[0]['key'] }}`. However, if you can at all
103
support multiple units on a relation, you should iterate over the list,
106
{% for unit in interface -%}
107
{{ unit['key'] }}{% if not loop.last %},{% endif %}
110
Note that since all sets of relation data from all related services and
111
units are in a single list, if you need to know which service or unit a
112
set of data came from, you'll need to extend this class to preserve
115
if not hookenv.relation_ids(self.name):
118
ns = self.setdefault(self.name, [])
119
for rid in sorted(hookenv.relation_ids(self.name)):
120
for unit in sorted(hookenv.related_units(rid)):
121
reldata = hookenv.relation_get(rid=rid, unit=unit)
122
if self._is_ready(reldata):
125
def provide_data(self):
127
Return data to be relation_set for this interface.
132
class MysqlRelation(RelationContext):
134
Relation context for the `mysql` interface.
136
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
137
:param list additional_required_keys: Extend the list of :attr:`required_keys`
142
def __init__(self, *args, **kwargs):
143
self.required_keys = ['host', 'user', 'password', 'database']
144
RelationContext.__init__(self, *args, **kwargs)
147
class HttpRelation(RelationContext):
149
Relation context for the `http` interface.
151
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
152
:param list additional_required_keys: Extend the list of :attr:`required_keys`
157
def __init__(self, *args, **kwargs):
158
self.required_keys = ['host', 'port']
159
RelationContext.__init__(self, *args, **kwargs)
161
def provide_data(self):
163
'host': hookenv.unit_get('private-address'),
168
class RequiredConfig(dict):
170
Data context that loads config options with one or more mandatory options.
172
Once the required options have been changed from their default values, all
173
config options will be available, namespaced under `config` to prevent
174
potential naming conflicts (for example, between a config option and a
177
:param list *args: List of options that must be changed from their default values.
180
def __init__(self, *args):
181
self.required_options = args
182
self['config'] = hookenv.config()
183
with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
184
self.config = yaml.load(fp).get('options', {})
187
for option in self.required_options:
188
if option not in self['config']:
190
current_value = self['config'][option]
191
default_value = self.config[option].get('default')
192
if current_value == default_value:
194
if current_value in (None, '') and default_value in (None, ''):
198
def __nonzero__(self):
199
return self.__bool__()
202
class StoredContext(dict):
204
A data context that always returns the data that it was first created with.
206
This is useful to do a one-time generation of things like passwords, that
207
will thereafter use the same value that was originally generated, instead
208
of generating a new value each time it is run.
210
def __init__(self, file_name, config_data):
212
If the file exists, populate `self` with the data from the file.
213
Otherwise, populate with the given data and persist it to the file.
215
if os.path.exists(file_name):
216
self.update(self.read_context(file_name))
218
self.store_context(file_name, config_data)
219
self.update(config_data)
221
def store_context(self, file_name, config_data):
222
if not os.path.isabs(file_name):
223
file_name = os.path.join(hookenv.charm_dir(), file_name)
224
with open(file_name, 'w') as file_stream:
225
os.fchmod(file_stream.fileno(), 0o600)
226
yaml.dump(config_data, file_stream)
228
def read_context(self, file_name):
229
if not os.path.isabs(file_name):
230
file_name = os.path.join(hookenv.charm_dir(), file_name)
231
with open(file_name, 'r') as file_stream:
232
data = yaml.load(file_stream)
234
raise OSError("%s is empty" % file_name)
238
class TemplateCallback(ManagerCallback):
240
Callback class that will render a Jinja2 template, for use as a ready
243
:param str source: The template source file, relative to
244
`$CHARM_DIR/templates`
246
:param str target: The target to write the rendered template to
247
:param str owner: The owner of the rendered file
248
:param str group: The group of the rendered file
249
:param int perms: The permissions of the rendered file
250
:param partial on_change_action: functools partial to be executed when
251
rendered file changes
253
def __init__(self, source, target,
254
owner='root', group='root', perms=0o444,
255
on_change_action=None):
261
self.on_change_action = on_change_action
263
def __call__(self, manager, service_name, event_name):
265
if self.on_change_action and os.path.isfile(self.target):
266
pre_checksum = host.file_hash(self.target)
267
service = manager.get_service(service_name)
269
for ctx in service.get('required_data', []):
271
templating.render(self.source, self.target, context,
272
self.owner, self.group, self.perms)
273
if self.on_change_action:
274
if pre_checksum == host.file_hash(self.target):
276
'No change detected: {}'.format(self.target),
279
self.on_change_action()
282
# Convenience aliases for templates
283
render_template = template = TemplateCallback